这篇博客是对前端面试所必须掌握的知识点的总结,并且这篇博客正在持续更新中…
面试复习
1.JavaScript 基础
1.执行上下文/作用域/闭包
1.什么是执行上下文?
执行上下文是评估和执行JavaScript代码环境的抽象概念。每当JavaScript代码在运行时,他都是在执行上下文中运行。
执行上下文的类型
JavaScript共有三种执行上下文类型
全局执行上下文这是基础的上下文,任何不在函数内部的代码都在全局上下文中.他会执行两件事:创建一个全局的window对象(浏览器环境的情况下),并且设置this的值等于这个全局对象。一个程序中只会有一个全局执行上下文函数执行上下文每当函数被调用时,都会为该函数创建一个新的执行上下文。每个函数都有他自己的执行上下文,只不过是在函数被调用时才被创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,他会按定义的瞬狙执行一系列步骤Eval函数执行上下文执行在eval
函数内部的代码也会有它属于自己的执行上下文
执行上下文栈
执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当JavaScript引擎第一次遇到你的脚本时,他会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,他会为该函数创建一个新的执行上下文并压入栈的顶部.
引擎会执行那些执行上下文位于栈顶的函数.每当函数执行结束之后,最上层的执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文
一旦所有代码执行完毕,JavaScript引擎从当前栈中移除全局执行上下文
怎么创建执行上下文?
创建执行上下文有两个阶段:
创建阶段执行阶段
创建阶段
在JavaScript代码执行前,执行上下文将经历创建阶段。在创建阶段将会发生三件事:
this值的绑定创建词法环境创建变量环境
所以执行上下文在概念上表示如下:
ExecutionContext = {ThisBinding = <this value>,LexicalEnvironment = { ... },VariableEnvironment = { ... },}
This绑定:
在全局执行上下文中,this
的值指向全局对象(在浏览器中,全局对象为window
’)
在函数执行上下文中,this的值取决于该函数是如何被调用的.如果他被一个引用类型对象调用,那么this会被设置成那个对象,否则this
的值被设置成全局对象或者undefined
(严格模式)
let foo = {baz: function() {console.log(this);}}foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被// 对象 'foo' 调用let bar = foo.baz;bar(); // 'this' 指向全局 window 对象,因为// 没有指定引用对象
词法环境
官方的 ES6 文档把词法环境定义为
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。
词法环境是一种持有变量符-变量映射的结构(标识符指的是变量/函数的名字,而变量是对实际对象或原始数据的引用)
在词法环境的内部有两个组件:1.环境记录器和2.一个外部环境的引用
1.环境记录器是存储变量和函数声明的实际位置
2.外部环境的引用意味着它可以访问其父级词法环境(作用域)
词法环境有两种类型:全局环境和函数环境
全局环境(在全局执行上下文中)是没有外部环境引用的词法环境,全局环境的外部环境引用是null,
它拥有创建的Object/Array等,在环境记录器内的原型函数(关联全局对象,比如window对象)还有任何用户定义的全局变量,并且this
的值指向全局对象
在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
环境记录器也有两种类型:
声明式环境记录器,用来存储变量、函数和参数对象环境记录器,用来定义出现在全局上下文中的变量和函数关系
由上不难得知
在全局环境中,环境记录器是对象环境记录器在函数环境中,环境记录器是声明式环境记录器
注意 :对于函数环境,声明式环境记录器还包含了一个传递给函数的arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的length。
抽象地讲,词法环境在伪代码中看起来像这样:
GlobalExectionContext = {LexicalEnvironment: {EnvironmentRecord: {Type: "Object",// 在这里绑定标识符}outer: <null>}}FunctionExectionContext = {LexicalEnvironment: {EnvironmentRecord: {Type: "Declarative",// 在这里绑定标识符}outer: <Global or outer function environment reference>}}
变量环境
变量环境其实也是一个词法环境,其环境记录器中持有变量声明语句在执行上下文中创建的绑定关系
变量环境有着词法环境的所有属性
在ES6中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let
和const
)绑定,而后者只用来存储var
变量绑定
我们看点样例代码来理解上面的概念:
let a = 20;const b = 30;var c;function multiply(e, f) {var g = 20;return e * f * g;}c = multiply(20, 30);
执行上下文看起来像这样:
GlobalExectionContext = {ThisBinding: <Global Object>,LexicalEnvironment: {EnvironmentRecord: {Type: "Object",// 在这里绑定标识符a: < uninitialized >,b: < uninitialized >,multiply: < func >}outer: <null>},VariableEnvironment: {EnvironmentRecord: {Type: "Object",// 在这里绑定标识符c: undefined,}outer: <null>}}FunctionExectionContext = {ThisBinding: <Global Object>,LexicalEnvironment: {EnvironmentRecord: {Type: "Declarative",// 在这里绑定标识符Arguments: {0: 20, 1: 30, length: 2},},outer: <GlobalLexicalEnvironment>},VariableEnvironment: {EnvironmentRecord: {Type: "Declarative",// 在这里绑定标识符g: undefined},outer: <GlobalLexicalEnvironment>}}
可能你已经注意到let
和const
定义的变量并没有关联任何值,但var
定义的变量被设成了undefined
。
这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为undefined
(var
情况下),或者未初始化(let
和const
情况下)。
这就是为什么你可以在声明之前访问var
定义的变量(虽然是undefined
),但是在声明之前访问let
和const
的变量会得到一个引用错误。
这就是我们说的变量声明提升。
执行阶段
在此阶段完成对所有存储的变量的分配,最后执行代码.
注意:在执行阶段,如果JavaScript引擎不能再源码中声明的实际位置找到let
变量的值,那么他就会被赋值为undefined
2.作用域(Scope)
什么是作用域?
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6之前,JavaScript只有全局作用域和函数作用域,ES6之后,新增了块级作用域,可以通过let
和const
来创建
全局作用域和函数作用域
**在代码中任何地方都能访问到的对象拥有全局作用域,**以下几种情况拥有全局作用域:
最外层函数和在最外层函数外定义的变量拥有全局作用域所有未定义直接赋值的变量默认为全局变量,拥有全局作用域所有window对象的属性拥有全局作用域
全局作用域的弊端:容易引发命名冲突,污染全局命名空间
函数作用域
在函数内部声明的变量拥有函数作用域,一般只能在固定的代码片段内可以访问到.
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行
值得注意的是:块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。
if (true) {// 'if' 条件语句块不会创建一个新的作用域var name = 'Hammad'; // name 依然在全局作用域中}console.log(name); // logs 'Hammad'
块级作用域
块级作用域可以通过let
和const
声明,所声明的变量在指定块作用域·之外无法被访问。
块级作用域在如下情况被创建:
在一个函数内部在一个代码块内部(‘{}’)
块级作用域有以下几个特点:
声明变量不会提升的代码块顶部禁止重复声明
作用域链
在JavaScript中,函数、块、模块都可以形成作用域,他们之间可以相互嵌套、作用域之间会形成引用关系,这条链叫做作用域链
作用域链的创建和变化
函数创建时:
JavaScript中使用的是词法作用域,函数的作用域在函数定义的时候就已经决定了
函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,可以理解为[[scope]]就是所有父变量对象的层级链,但是注意:[[scope]]并不代表完整的作用域链
举个例子:
function foo() {function bar() {...}}
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [globalContext.VO];bar.[[scope]] = [fooContext.AO,globalContext.VO];
函数被激活时:
当函数被激活时,进入函数上下文,创建VO/AO后就会将活动对象添加到作用域的前端
这时候执行上下文的作用域链,我们命名为Scope
Scope = [AO].concat([[Scope]]);
至此,作用域链创建完毕
3.闭包
什么是闭包?
闭包就是同时含有对函数对象以及作用域对象引用的对象,实际上所有JavaScript对象都是闭包.
本质:在一个函数内部创建另一个函数
只要存在函数嵌套,并且内部函数调用了外部函数的属性,就产生了闭包.
闭包的特性:
函数嵌套函数函数内部引用函数外部的参数和变量参数和变量不会被垃圾回收机制回收
闭包是什么时候被创建的?
因为所有JavaScript对象都是闭包,所以当你定义一个函数时.就产生了闭包
闭包是什么时候被销毁的?
当他不被任何其他的对象引用的时候,闭包就被销毁
闭包的好处:
保护函数内的变量安全,实现封装,防止变量流入其他环境发生命名冲突在内存中维持一个变量,延长变量的生命周期匿名自执行函数可以减少内存消耗
闭包的缺点:
被引用的私有变量不能被销毁,增大了内存的消耗,造成内存泄露闭包涉及跨域访问,会导致性能损失
闭包的作用
使得函数内部的变量在函数执行完之后,仍然存活在内存中(延长了局部变量的生命周期)让函数外部可以操作到函数内部的数据
闭包的原理
当一个函数返回后,没有其他对象会保存对其的引用。所以,它就可能被垃圾回收器
回收。
函数对象中总是有一个[[scope]]
属性,保存着该函数被定义的时候所能够直接访问的作用域对象。所以,当我们在定义嵌套的函数的时候,这个嵌套的函数的[[scope]]
就会引用外围函数(Outer function)的当前作用域对象。
如果我们将这个嵌套函数返回,并被另一个标识符所引用的话,那么这个嵌套函数及其[[scope]]所引用的作用作用域对象就不会被垃圾回收器所销毁,这个对象就会一直存活在内存中,我们可以通过这个作用于对象获取到外部函数的属性和值。
这就是闭包的原理
2.this/call/apply/bind
1.this的指向
函数的this是在函数调用时才绑定的,它的指向完全取决于函数的调用位置(也就是函数的调用方法),为了搞清楚
this
的指向是什么,必须知道相关函数是如何调用的
在全局上下文中:
非严格模式和严格模式中this都指向顶层对象(在浏览器中是window
)
this === window // true'use strict'this === window;this.name = '若川';console.log(this.name); // 若川
普通函数调用模式
// 非严格模式var name = 'window';var doSth = function(){console.log(this.name);}doSth(); // 'window'复制代码
你可能会误以为window.doSth()
是调用的,所以是指向window
。虽然本例中window.doSth
确实等于doSth
。name
等于window.name
。上面代码中这是因为在ES5
中,全局变量是挂载在顶层对象(浏览器是window
)中。 事实上,并不是如此。
// 非严格模式let name2 = 'window2';let doSth2 = function(){console.log(this === window);console.log(this.name2);}doSth2() // true, undefined复制代码
这个例子中let
没有给顶层对象中(浏览器是window)添加属性,window.name2和window.doSth
都是undefined
。
严格模式中,普通函数中的this
则表现不同,表现为undefined
。
// 严格模式'use strict'var name = 'window';var doSth = function(){console.log(typeof this === 'undefined');console.log(this.name);}doSth(); // true,// 报错,因为this是undefined复制代码
看过的《你不知道的JavaScript
》上卷的读者,应该知道书上将这种叫做默认绑定。 对call
,apply
熟悉的读者会类比为:
doSth.call(undefined);doSth.apply(undefined);复制代码
效果是一样的,call
,apply
作用之一就是用来修改函数中的this
指向为第一个参数的。 第一个参数是undefined
或者null
,非严格模式下,是指向window
。严格模式下,就是指向第一个参数。后文详细解释。
经常有这类代码(回调函数),其实也是普通函数调用模式。
var name = '若川';setTimeout(function(){console.log(this.name);}, 0);// 语法setTimeout(fn | code, 0, arg1, arg2, ...)// 也可以是一串代码。也可以传递其他函数// 类比 setTimeout函数内部调用fn或者执行代码`code`。fn.call(undefined, arg1, arg2, ...);复制代码
对象中的函数(方法)调用模式
var name = 'window';var doSth = function(){console.log(this.name);}var student = {name: '若川',doSth: doSth,other: {name: 'other',doSth: doSth,}}student.doSth(); // '若川'student.other.doSth(); // 'other'// 用call类比则为:student.doSth.call(student);// 用call类比则为:student.other.doSth.call(student.other);复制代码
但往往会有以下场景,把对象中的函数赋值成一个变量了。 这样其实又变成普通函数了,所以使用普通函数的规则(默认绑定)。
var studentDoSth = student.doSth;studentDoSth(); // 'window'// 用call类比则为:studentDoSth.call(undefined);复制代码
call、apply、bind
调用模式
上文提到call
、apply
,这里详细解读一下。先通过MDN
认识下call
和apply
MDN 文档:Function.prototype.call()
语法
fun.call(thisArg, arg1, arg2, ...)复制代码
thisArg
在fun
函数运行时指定的this
值。需要注意的是,指定的this
值并不一定是该函数执行时真正的this
值,如果这个函数处于非严格模式下,则指定为null
和undefined
的this
值会自动指向全局对象(浏览器中就是window
对象),同时值为原始值(数字,字符串,布尔值)的this
会指向该原始值的自动包装对象。
arg1, arg2, …
指定的参数列表
返回值
返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined
。
apply
和call
类似。只是参数不一样。它的参数是数组(或者类数组)。
根据参数thisArg
的描述,可以知道,call
就是改变函数中的this
指向为thisArg
,并且执行这个函数,这也就使JS
灵活很多。严格模式下,thisArg
是原始值是值类型,也就是原始值。不会被包装成对象。举个例子:
var doSth = function(name){console.log(this);console.log(name);}doSth.call(2, '若川'); // Number{2}, '若川'var doSth2 = function(name){'use strict';console.log(this);console.log(name);}doSth2.call(2, '若川'); // 2, '若川'复制代码
虽然一般不会把thisArg
参数写成值类型。但还是需要知道这个知识。 之前写过一篇文章:面试官问:能否模拟实现JS
的call
和apply
方法 就是利用对象上的函数this
指向这个对象,来模拟实现call
和apply
的。感兴趣的读者思考如何实现,再去看看笔者的实现。
bind
和call
和apply
类似,第一个参数也是修改this
指向,只不过返回值是新函数,新函数也能当做构造函数(new
)调用。 MDN Function.prototype.bind
bind()
方法创建一个新的函数, 当这个新函数被调用时this
键值为其提供的值,其参数列表前几项值为创建时指定的参数序列。
语法:fun.bind(thisArg[, arg1[, arg2[, …]]])
参数:thisArg调用绑定函数时作为this参数传递给目标函数的值。 如果使用new
运算符构造绑定函数,则忽略该值。当使用bind
在setTimeout
中创建一个函数(作为回调提供)时,作为thisArg
传递的任何原始值都将转换为object
。如果没有提供绑定的参数,则执行作用域的this
被视为新函数的thisArg
。arg1, arg2, …当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。返回值返回由指定的this
值和初始化参数改造的原函数拷贝。
构造函数调用模式
function Student(name){this.name = name;console.log(this); // {name: '若川'}// 相当于返回了// return this;}var result = new Student('若川');复制代码
使用new
操作符调用函数,会自动执行以下步骤。
创建了一个全新的对象。这个对象会被执行[[Prototype]]
(也就是__proto__
)链接。生成的新对象会绑定到函数调用的this
。通过new
创建的每个对象将最终被[[Prototype]]
链接到这个函数的prototype
对象上。如果函数没有返回对象类型Object
(包含Functoin
,Array
,Date
,RegExg
,Error
),那么new
表达式中的函数调用会自动返回这个新的对象。
由此可以知道:new
操作符调用时,this
指向生成的新对象。特别提醒一下,new
调用时的返回值,如果没有显式返回对象或者函数,才是返回生成的新对象。
function Student(name){this.name = name;// return function f(){};// return {};}var result = new Student('若川');console.log(result); {name: '若川'}// 如果返回函数f,则result是函数f,如果是对象{},则result是对象{}复制代码
很多人或者文章都忽略了这一点,直接简单用typeof
判断对象。虽然实际使用时不会显示返回,但面试官会问到。
总结
如果要判断一个运行中的函数的this绑定,就需要找到这个函数的直接调用位置.找到之后就可以顺序应用下面这四条规则来判断this的指向
普通函数调用:在严格模式下绑定到undefined
,否则绑定到全局对象构造函数形式调用:绑定到新创建的实例对象对象上的函数调用:帮到到那个对象call、apply、bind调用:在非严格模式下,this为函数传入的第一个参数,如果第一个参数为null
或者undefined
,this
会指向全局对象(浏览器中就是window对象)
箭头函数的this:不会使用上文的四条标准的绑定规则,而是根据当前的词法作用域来决定this。
箭头函数没有自己的this、super、argument和new.target绑定,所以必须通过查找作用域链来决定其值.如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值会被设置位全局对象.
2.call
call()方法的作用是在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法
举个栗子
var foo = {value: 1};function bar() {console.log(this.value);}bar.call(foo); // 1
手写实现call方法
Function.prototype.ts_call(obj,...args){obj = obj || window;const fn = Symbol(); //创建一个唯一变量,防止属性名冲突obj[fn] = this; //将属性指定为目标函数obj[fn](...args);//执行函数delete obj[fn];//执行后将这个属性删除}function fn(age){console.log(`我叫${this.name}今年${age}岁了!`);}const testobj2 = {name: 'zzm'}fn.cs_call(testobj2,18)//我叫zzm今年18岁了!
3.apply
apply的方法与call类似,不同之处在于apply参数以数组的方式传递,所以call能实现的需求,用apply也同样可以
手写实现apply方法
Function.prototype.cs_apply(obj,arg){obj = obj || window;const fn = Symbol();obj[fn] = this;obj[fn](...args);delete obj[fn];}function fn(age,hobby){console.log(`我叫${this.name}今年${age}岁了我喜欢${hobby}`);}const testobj2 = {name: 'zzm'}fn.cs_apply(testobj2,[18,'睡觉'])//我叫zzm今年18岁了我喜欢睡觉
4.bind
bind是ES5新增的一个方法传参和call或apply类似不会执行对应的函数,call或apply会自动执行对应的函数返回对函数的引用bind()函数会创建一个新函数(称之为绑定函数)
**下面例子:**当点击网页时,EventClick
被触发执行,输出JSLite.io p1 p2
, 说明EventClick
中的this
被bind
改变成了obj
对象。如果你将EventClick.bind(obj,'p1','p2')
变成EventClick.call(obj,'p1','p2')
的话,页面会直接输出JSLite.io p1 p2
var obj = {name:'JSLite.io'};/** * 给document添加click事件监听,并绑定EventClick函数 * 通过bind方法设置EventClick的this为obj,并传递参数p1,p2 */document.addEventListener('click',EventClick.bind(obj,'p1','p2'),false);//当点击网页时触发并执行function EventClick(a,b){ console.log( this.name, //JSLite.io a, //p1 b //p2 )}// JSLite.io p1 p2
手写bind
Function.prototype.cs_bind = function(obj,...args){obj = obj || window; const fn = Symbol(); obj[fn] = this; const _this = this; const res = function(...innerArgs){if(this instanceof _this){//当作构造函数使用 this[fn] = _this; this[fn](...[...args,...innerArgs]); delete this[fn]; }else{//没有当作构造函数使用 obj[fn](...[...args,...innerArgs]) delete obj[fn]; } } res.prototype = Object.create(this.prototype); return res;}
3.原型/继承
1.原型
原型链经典神图
function Foo 就是一个方法,比如JavaScript 中内置的 Array、String 等function Object 就是一个 Objectfunction Function 就是 Function以上都是 function,所以.__proto__
都是Function.prototype
再次强调,String、Array、Number、Function、Object都是 function
prototype的定义
在规范里,prototype被定义为:给其他对象提供共享属性的对象
也就是说prototype自己也是对象,只是被用来承担某个只能罢了
prototype描述的是两个对象之间的某种关系(其中一个对象为另一个对象提供属性访问权限).所有对象都可以作为另一个对象的prototype来使用
函数对象和普通对象
在JavaScript中,万物皆对象,但是不同的对象是存在着差异性的.
在JavaScript中,我们将对象分为函数对象和普通对象,函数对象就是JavaScript用函数来模拟的类实现,Object和Function就是典型的函数对象
function fun1(){};const fun2 = function(){};const fun3 = new Function('name','console.log(name)');const obj1 = {};const obj2 = new Object();const obj3 = new fun1();const obj4 = new new Function();console.log(typeof Object);//functionconsole.log(typeof Function);//functionconsole.log(typeof fun1);//functionconsole.log(typeof fun2);//functionconsole.log(typeof fun3);//functionconsole.log(typeof obj1);//objectconsole.log(typeof obj2);//objectconsole.log(typeof obj3);//objectconsole.log(typeof obj4);//object
上述代码中,obj1
,obj2
,obj3
,obj4
都是普通对象,fun1
,fun2
,fun3
都是Function
的实例,也就是函数对象。
总结:所有的Function的实例都是函数对象,其他的均为普通对象,包括Function实例的实例
JavaScript中万物皆对象,而对象皆出自构造函数
对于Function对象:
Function.__proto__ === Function.prototype //true
__ proto__
首先我们需要明确:
__ proto__和constructor是对象独有的prototype属性是函数独有的
但是在JavaScript中,函数也是一种特殊的对象,所以函数也拥有__proto__
和constructor
属性
结合上面我们介绍的Object
和Function
的关系,看一下代码和关系图
function Person(){…}; let nealyang = new Person();
再梳理上图关系之前,我们再来讲解下__proto__
。
这里我们需要知道的是,__proto__
是对象所独有的,并且__proto__
是一个对象指向另一个对象,也就是他的原型对象。我们也可以理解为父类对象。它的作用就是当你在访问一个对象属性的时候,如果该对象内部不存在这个属性,那么就回去它的__proto__
属性所指向的对象(父类对象)上查找,如果父类对象依旧不存在这个属性,那么就回去其父类的__proto__
属性所指向的父类的父类上去查找。以此类推,知道找到null
。而这个查找的过程,也就构成了我们常说的原型链。
原型链就是根据对象的__ proto __指向,一层一层连接起来的具有关联性的对象集合
prototype
prototype 被定义为:给其它对象提供共享属性的对象。prototype
自己也是对象,只是被用以承担某个职能罢了
所有对象都可以作为另一个对象的prototype使用
任何函数在创建的时候,都会默认给该函数添加prototype
属性.
constructor
constructor属性也是对象所独有的,他是一个对象指向同一个函数,这个函数就是该对象的构造函数
每个对象都有其对应的构造函数,它由本身或者继承而来.
函数.prototype.constructor===该函数本身
constructor属性只有prototype对象才有,函数在创建的时候,JavaScript会同时创建一个该函数对应的prototype对象,而函数创建的对象.proto === 该函数.prototype
通过函数创建的对象即使自己没有constructor
属性,它也能通过__proto__
找到对应的constructor
,所以任何对象最终都可以找到其对应的构造函数。
原型链
原型链就是根据对象的__ proto __指向,一层一层连接起来的具有关联性的对象集合
typeof&&instanceof原理
typeof
用于判断变量的类型,可以判断的类型有:number、undefined、String、Boolean、function、object、symbol,但是typeof在判断object时不能明确的告诉你属于哪一类object
所以一般不用typeof来判断object的类型
为什么typeof null返回’object’?
因为null代表的是空指针对象,所以typeof null 为object
具体原因:在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于null
代表的是空指针(大多数平台下值为 0x00),因此,null
的类型标签是 0,typeof null
也因此返回"object"
instanceof
instanceof
运算符用来检测constructor.prototype
是否存在于参数object
的原型链上。与typeof
方法不同的是,instanceof
方法要求开发者明确地确认对象为某特定类型。
instanceof可以判断一个实例是否是其父类型或者祖先类型的实例。
instanceof是如何进行判断的?
表达式**:A instanceof B**:如果B的显式原型(prototype)对象在A的原型链上,返回true,否则返回false
手写instanceof
function _instanceof(child,father){const fp = father.prototype let cp = child.__proto__ while(cp){if(cp.__proto__ === father.protype){return true; } cp = cp.__proto__; } return false; }
console.log(Object instanceof Object);//true console.log(Function instanceof Function);//true console.log(Number instanceof Number);//false console.log(String instanceof String);//false console.log(Function instanceof Object);//true console.log(Foo instanceof Function);//true console.log(Foo instanceof Foo);//false
为什么Object
和Function
instanceof
自己等于true
,而其他类instanceof
自己却又不等于true
呢?如何解释?
Object instanceof Object
// 为了方便表述,首先区分左侧表达式和右侧表达式ObjectL = Object, ObjectR = Object; // 下面根据规范逐步推演O = ObjectR.prototype = Object.prototype L = ObjectL.__proto__ = Function.prototype // 第一次判断O != L // 循环查找 L 是否还有 __proto__ L = Function.prototype.__proto__ = Object.prototype // 第二次判断O == L // 返回 true
Function instanceof Function
// 为了方便表述,首先区分左侧表达式和右侧表达式FunctionL = Function, FunctionR = Function; // 下面根据规范逐步推演O = FunctionR.prototype = Function.prototype L = FunctionL.__proto__ = Function.prototype // 第一次判断O == L // 返回 true
Foo instanceof Foo
// 为了方便表述,首先区分左侧表达式和右侧表达式FooL = Foo, FooR = Foo; // 下面根据规范逐步推演O = FooR.prototype = Foo.prototype L = FooL.__proto__ = Function.prototype // 第一次判断O != L // 循环再次查找 L 是否还有 __proto__ L = Function.prototype.__proto__ = Object.prototype // 第二次判断O != L // 再次循环查找 L 是否还有 __proto__ L = Object.prototype.__proto__ = null // 第三次判断L == null // 返回 false
2.继承
在JavaScript中,有两类原型继承的方式:显式继承和隐式继承
new
new用来创建构造函数的实例对象
手写new
function myNew(fn,...args){let obj = {}; obj.__proto__ = fn.prototype;//将obj的__proto__赋值为fn的prototype fn.apply(obj,args);//将构造函数的this指向这个对象 return obj; }
类式继承
function SuperClass() {this.superValue = true;}SuperClass.prototype.getSuperValue = function(){return this.superValue;}function SubClass() {this.subValue = false;}SubClass.prototype = new SuperClass();SubClass.prototype.getSubValue = function(){return this.subValue;}const instance = new SubClass();console.log( instance instanceof SuperClass);//trueconsole.log( instance instanceof SubClass);//trueconsole.log(SubClass instanceof SuperClass);//false
虽然实现起来清晰简洁,但是这种继承方式有两个缺点:
由于子类通过其原型prototype对父类实例化,继承了父类,所以说父类中如果共有属性是引用类型,就会在子类中被所有的实例所共享,因此一个子类的实例更改子类原型从父类构造函数中继承的共有属性就会直接影响到其他的子类由于子类实现的继承是靠其原型prototype对父类进行实例化实现的,因此在创建父类的时候,是无法向父类传递参数的。因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化
构造函数继承
function SuperClass(id) {this.books = ['js','css']; this.id = id;}SuperClass.prototype.showBooks = function(){console.log(this.books);}function SubClass(id){SuperClass.call(this,id)}const instance1 = new SubClass(10);const instance2 = new SubClass(10);instance1.books.push('html');console.log(instance1)console.log(instance2)instance1.showBooks();//TypeError
SuperClass.call(this,id)
当然就是构造函数继承的核心语句了.由于父类中给this绑定属性,因此子类自然也就继承父类的共有属性。由于这种类型的继承没有涉及到原型prototype
,所以父类的原型方法自然不会被子类继承,而如果想被子类继承,就必须放到构造函数中,这样创建出来的每一个实例都会单独的拥有一份而不能共用,这样就违背了代码复用的原则,所以综合上述两种,我们提出了组合式继承方法
组合式继承
function SuperClass(name) {this.name = name; this.books = ['Js','CSS'];}SuperClass.prototype.getBooks = function() {console.log(this.books);}function SubClass(name,time) {SuperClass.call(this,name); this.time = time;}SubClass.prototype = new SuperClass();SubClass.prototype.getTime = function() {console.log(this.time);}
如上,我们就解决了之前说到的一些问题,但是是不是从代码看,还是有些不爽呢?至少这个SuperClass
的构造函数执行了两遍就感觉非常的不妥.
原型式继承
function inheritObject(o) {//声明一个过渡对象 function F() { } //过渡对象的原型继承父对象 F.prototype = o; //返回过渡对象的实例,该对象的原型继承了父对象 return new F();}
原型式继承大致的实现方式如上,是不是想到了我们new
关键字模拟的实现?
其实这种方式和类式继承非常的相似,他只是对类式继承的一个封装,其中的过渡对象就相当于类式继承的子类,只不过在原型继承中作为一个普通的过渡对象存在,目的是为了创建要返回的新的实例对象。
var book = { name:'js book', likeBook:['css Book','html book']}var newBook = inheritObject(book);newBook.name = 'ajax book';newBook.likeBook.push('react book');var otherBook = inheritObject(book);otherBook.name = 'canvas book';otherBook.likeBook.push('node book');console.log(newBook,otherBook);复制代码
如上代码我们可以看出,原型式继承和类式继承一个样子,对于引用类型的变量,还是存在子类实例共享的情况。
所以,我们还有下面的寄生式继承
寄生式继承
var book = {name:'js book', likeBook:['html book','css book']}function createBook(obj) {//通过原型方式创建新的对象 var o = new inheritObject(obj); // 拓展新对象 o.getName = function(name) { console.log(name) } // 返回拓展后的新对象 return o;}
其实寄生式继承就是对原型继承的拓展,一个二次封装的过程,这样新创建的对象不仅仅有父类的属性和方法,还新增了别的属性和方法。
寄生组合式继承
回到之前的组合式继承,那时候我们将类式继承和构造函数继承组合使用,但是存在的问题就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了寄生组合式继承
而寄生组合式继承是寄生式继承和构造函数继承的组合。但是这里寄生式继承有些特殊,这里他处理不是对象,而是类的原型。
function inheritObject(o) {//声明一个过渡对象 function F() { } //过渡对象的原型继承父对象 F.prototype = o; //返回过渡对象的实例,该对象的原型继承了父对象 return new F();}function inheritPrototype(subClass,superClass) { // 复制一份父类的原型副本到变量中 var p = inheritObject(superClass.prototype); // 修正因为重写子类的原型导致子类的constructor属性被修改 p.constructor = subClass; // 设置子类原型 subClass.prototype = p;}
组合式继承中,通过构造函数继承的属性和方法都是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。
我们需要继承的仅仅是父类的原型,不用去调用父类的构造函数。换句话说,在构造函数继承中,我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们可以通过原型继承拿到,但是这么直接赋值给子类会有问题,因为对父类原型对象复制得到的复制对象p中的constructor
属性指向的不是subClass
子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复起constructor
属性指向性不正确的问题,最后将得到的复制对象p赋值给子类原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。
function SuperClass(name) {this.name = name; this.books=['js book','css book'];}SuperClass.prototype.getName = function() {console.log(this.name);}function SubClass(name,time) {SuperClass.call(this,name); this.time = time;}inheritPrototype(SubClass,SuperClass);SubClass.prototype.getTime = function() {console.log(this.time);}var instance1 = new SubClass('React','/11/11')var instance2 = new SubClass('Js','/22/33');instance1.books.push('test book');console.log(instance1.books,instance2.books);instance2.getName();instance2.getTime();
这种方式继承其实如上图所示,其中最大的改变就是子类原型中的处理,被赋予父类原型中的一个引用,这是一个对象,因此有一点你需要注意,就是子类在想添加原型方法必须通过prototype.来添加,否则直接赋予对象就会覆盖从父类原型继承的对象了.
4.promise
1.什么是promise?它用来解决什么问题?
Promise是异步编程的一种解决方案:从语法上讲,promise是一个对象,可以通过它获取异步操作的消息;从本意上讲,他是承诺,承诺他过一段时间会给你一个结果。promise有三种状态:pending(等待态),fulfiled(成功态),rejected(失败态);状态一旦改变,就不会再改变(也就是说promise的操作是不可逆的),创造promise实例后,他会立即执行
promise是用来解决两个问题:
回调地狱,减少多层回调嵌套异步执行(但是不能说promise是异步的)
2.promise用法
创建promise对象
promise是一个构造函数,可以通过new来创建实例对象
Promise的构造函数接收一个参数:函数,并且这个函数需要传入两个参数:
resolve:异步操作执行成功后的回调函数reject:异步操作执行失败后的回调函数
let p = new Promise((resolve, reject) => {//做一些异步操作 setTimeout(() => { console.log('执行完成'); resolve('我是成功!!'); }, 2000);});
then链式调用
promise可以通过链式调用来减少多层回调嵌套
const p = new Promise((resolve,rehect) =>{resolve('ok');});p.then((data) => {console.log(data);}).then((data) => {console.log(data);}).then((data) => {console.log(data);});
reject的用法
把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调
let p = new Promise((resolve, reject) => {//做一些异步操作setTimeout(function(){ var num = Math.ceil(Math.random()*10); //生成1-10的随机数 if(num<=5){resolve(num); } else{reject('数字太大了'); }}, 2000); }); p.then((data) => { console.log('resolved',data); },(err) => { console.log('rejected',err); } );
then中传了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到下面两种结果:
或者
catch的用法
catch的作用与then的第二个参数类似,用于捕获失败的回调,不过与后者不同的是,在链式调用时,如果代码出错了,他不会报错使js执行停止,而是会进入到catch方法中,并捕获到异常
p.then((data) => {console.log('resolved',data); console.log(somedata); //此处的somedata未定义}).catch((err) => { console.log('rejected',err);});
在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:
也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能
Promise.all()
用法:接受一个数组参数,里面的值最终都返回Promise对象
特点:谁执行慢,以谁为准执行回调
let Promise1 = new Promise(function(resolve, reject){})let Promise2 = new Promise(function(resolve, reject){})let Promise3 = new Promise(function(resolve, reject){})let p = Promise.all([Promise1, Promise2, Promise3])p.then(funciton(){// 三个都成功则成功 }, function(){ // 只要有失败,则失败 })
应用场景:一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。
Promise.race()
用法:接受一个数组参数,里面的值最终都返回Promise对象
特点:谁执行快,以谁为准执行回调
const promise1 = new Promise(function(resolve, reject) {// resolve(1); reject(1);})const promise2 = new Promise(function(resolve, reject) { resolve(2);})const promise3 = new Promise(function(resolve, reject) { resolve(3);})const p = Promise.race([promise1,promise2,promise3])p.then((data) => { console.log("data",data);}).catch((err) => { console.log("err",err);})
Promise.any()
注意:Promise.any()尚未被所有浏览器所支持,node环境下不能使用这个API
用法:接受一个数组参数,里面的值最终都返回Promise对象
特点:只要有一个promise执行成功,那么就返回那个成功的promise
const promise1 = new Promise(function(resolve, reject) {// resolve(1); reject(1);})const promise2 = new Promise(function(resolve, reject) { resolve(2);})const promise3 = new Promise(function(resolve, reject) { resolve(3);})const p = Promise.any([promise1,promise2,promise3])p.then((data) => { console.log("data",data);}).catch((err) => { console.log("err",err);})
3.async/await
1.什么是async?
介绍:async函数是使用async
关键字声明的函数。 async函数是AsyncFunction
构造函数的实例, 并且其中允许使用await
关键字。async
和await
关键字让我们可以用一种更简洁的方式写出基于Promise
的异步行为,而无需刻意地链式调用promise
。
特性:async函数可能包含0个或者多个await
表达式。await表达式会暂停整个async函数的执行进程并出让其控制权,只有当其等待的基于promise的异步操作被兑现或被拒绝之后才会恢复进程。promise的解决值会被当作该await表达式的返回值。使用async
/await
关键字就可以在异步代码中使用普通的try
/catch
代码块。
async
函数是Generator
函数的语法糖。使用 关键字async
来表示,在函数内部使用await
来表示异步。相较于Generator
,async
函数的改进在于下面四点:
内置执行器。Generator
函数的执行必须依靠执行器,而async
函数自带执行器,调用方式跟普通函数的调用一样更好的语义。async
和await
相较于*
和yield
更加语义化更广的适用性。co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise对象。而async
函数的await
命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作)返回值是 Promise。async
函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用then()
方法进行调用
async
是ES7新出的特性,表明当前函数是异步函数,不会阻塞线程导致后续代码停止运行。
2.async函数怎么用?
async用来声明函数是一个异步函数
await表示紧跟在后面的表达式需要等待结果
async function asyncFn(){return 'hello world';}asuncFn();
async函数返回的是一个promise对象,状态为resolved,参数是return的值,所以async函数可以链式调用
async function asyncFn() {return '我后执行'}asyncFn().then(result => {console.log(result);//我后执行})console.log('我先执行');
async函数返回的是一个promise对象,如果再执行过程中函数内部抛出异常或者返回reject,都会是的函数的promise状态变为失败rejected,函数抛出异常后,可以通过catch接收到返回的错误信息
async function asyncFn() {return Promise.reject('reason') // throw new Error('has error')}asyncFn().then(result => { console.log(result);},reason => { console.log(reason);}).catch(err => { console.log(err);})console.log('我先执行');
async函数接收到的返回值,如果不是异常或者reject,则判定成功,即resolve
以下结果会使async函数判定失败:
内部含有直接使用并且未声明的变量或者函数。内部抛出一个错误throw new Error
或者返回reject
状态return Promise.reject('执行失败')
函数方法执行出错(🌰:Object使用push())等等…
async函数如果需要返回结果,都必须使用return来返回,不论是reject还是resolve都需要使用return,不然就会返回一个值为undefined的resolved(成功)状态
3.await是什么
await的意思是async wait(异步等待),await必须配合async使用,async函数必须等到内部所有的await命令的promise执行完,才会返回结果
打个比方,await是学生,async是校车,必须等人齐了再开车。
就是说,必须等所有await
函数执行完毕后,才会告诉promise
我成功了还是失败了,执行then
或者catch
async function awaitReturn() { return await 1};awaitReturn().then(success => console.log('成功', success)) .catch(error => console.log('失败',error))
async中的await会返回一个promise,下一个await必须等待上一个await返回promise结果状态才会开始执行
let time1;let time2;setTimeout(() => {time1 = new Date().getTime(); console.log("第一个函数执行完毕");}, 1000);setTimeout(() => {time2 = new Date().getTime(); console.log("第二个函数执行完毕",time2-time1);}, 2000);const timeoutFn = function(timeout){return new Promise(function(resolve){return setTimeout(resolve, timeout); });}async function timeOut() {await timeoutFn(1000); await timeoutFn(2000); console.log("完成");}timeOut()
不考虑event loop(事件循环)
上面的例子中 两个setTimeout函数会在2s左右(之所以说左右这个词,是因为函数执行会消耗几毫秒时间),
而timeOut函数则需要3s左右的时间才会执行完成,这也说明了下一个await必须等待上一个await返回promise结果状态才会开始执行的结论是正确的
await后面的表达式应该返回一个promise,如果不是promise,js内部也会将其转换为一个resolved状态的 promise
5.深浅拷贝
JavaScript的数据类型分为基本数据类型和引用数据类型。
对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言的。
什么是浅拷贝?
浅拷贝的意思就是只复制引用,而未复制真正的值。
当我们浅拷贝一个数组或者对象后,改变这个新的数组或对象,那么被我们拷贝的数组和对象也会改变
const originArray = [1,2,3,4,5];//数组是引用类型const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};//对象是引用类型let originType = [1,2,3];let originNumber = 1; //基本值类型,不存在深浅拷贝之分const clonedArray = originArray;const clonedObj = originObj;let cloneNumber = originNumber;let cloneType = originType;clonedArray.push(6);clonedObj.a = 'aa';cloneNumber = 2;cloneType = {a:'a',b:'b',b:'c'}console.log(originArray);//[ 1, 2, 3, 4, 5, 6 ]console.log(originObj);//{ a: 'aa', b: 'b', c: [ 1, 2, 3 ], d: { dd: 'dd' } }console.log(originNumber);//1console.log(originType);//当我们改变了变量引用的类型时,这个新变量和被拷贝的变量就没有了任何联系(二者指向不同引用)
什么是深拷贝?
深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。
只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。
目前实现深拷贝的方法不多,主要是两种:
利用JSON
对象中的parse
和stringify
利用递归来实现每一层都重新创建对象并赋值
利用JSON.stringify/parse的方法实现深拷贝
JSON.stringify的作用是将一个javascript值转换成json字符串
JSON.parse的作用是将一个JSON字符串转换成javascript值或对象
const originArray = [1,2,3,4,5];const cloneArray = JSON.parse(JSON.stringify(originArray));console.log(cloneArray === originArray); // falseconst originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};const cloneObj = JSON.parse(JSON.stringify(originObj));console.log(cloneObj === originObj); // falsecloneObj.a = 'aa';cloneObj.c = [1,1,1];cloneObj.d.dd = 'doubled';console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
上面的例子可以实现深拷贝,但是这种方式只能适用于一些简单的情况,因为在使用HSON.stringify()时,undefined
、function
、symbol
会在转换过程中被忽略。。
如果对象中含有以上几种类型时,就不能用这个方法进行深拷贝。
const originObj = {name:'axuebin', sayHello:function(){console.log('Hello World'); }}console.log(originObj); // {name: "axuebin", sayHello: ƒ}const cloneObj = JSON.parse(JSON.stringify(originObj));console.log(cloneObj); // {name: "axuebin"}
使用递归的方法实现深拷贝
递归的思想就很简单了,就是对每一层的数据都实现一次创建对象->对象赋值
的操作
//实现深拷贝function deepClone(source) { const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象 for (const key in source) { if (source.hasOwnProperty(key)) { if(source[key] && typeof source[key] === 'object'){//如果值是对象就递归targetObj[key] = source[key].constructor === 'Array' ? [] : {};deepClone(targetObj[key]); }else{//如果不是就直接赋值targetObj[key] = source[key]; }} } return targetObj;}const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};const cloneObj = deepClone(originObj);console.log(cloneObj === originObj); // falsecloneObj.a = 'aa';cloneObj.c = [1,1,1];cloneObj.d.dd = 'doubled';console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};const originObj2 = { name:'张振明', sayHello:function(){console.log('Hello World'); } } console.log(originObj2); // {name: "张振明", sayHello: ƒ} const cloneObj2 = deepClone(originObj2); console.log(cloneObj2); // {name: "张振明", sayHello: ƒ}
JavaScript中的拷贝方法
JavaScript数组中有两个方法,concat和slice,他们都不会改变原数组,而是返回一个新数组
所以他们是可以实现对原数组的拷贝的,另外es6新增的Object.assgn
方法和...
展开运算符也能实现对对象的拷贝
这里只说明结论,不解释详细过程
concat
该方法可以连接两个或者更多的数组,但是它不会修改已存在的数组,而是返回一个新数组。
结论:concat
只是对数组的第一层进行深拷贝。
slice
结论:slice
只是对数组的第一层进行深拷贝。
Object.assign()
结论:Object.assign()
拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值
… 展开运算符
结论:...
实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。
总结
赋值运算符=
实现的是浅拷贝,只拷贝对象的引用值;JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;JSON.stringify
实现的是深拷贝,但是对目标对象有要求;若想真正意义上的深拷贝,请递归。6.event loop(事件循环)
在学习事件循环之前,我们应该明白:JavaScript本质上就是一个单线程语言,一切JavaScript的所谓“多线程”都是用单线程模拟出来的
什么是事件循环?
因为js是单线程的,js的任务是按顺序一个一个执行的。但一个任务耗时过长,后面的任务将被阻塞,这是我们不想看到的。所以程序员将任务分为两类:
同步任务异步任务
任务执行机制:
导图要表达的内容用文字来表述的话:
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的Event Loop(事件循环)。
事件循环的一些举例
setTimeout
setTimeout表示执行一个延时函数,他的特点是可以异步延时执行
setTimeout(() => {console.log('延时3秒');},3000)
但是在有些情况下,延时并不准确
setTimeout(() => {task()},3000)sleep(10000000)//消耗很多的时间
上例中,sleep()函数是一个同步任务,他直接在主线程中执行,而setTimeout是个异步任务,在任务执行过程中,task()执行的时间却远远大于3s,这时候延时并不准确,这是什么原因?
task()
进入Event Table并注册,计时开始。执行sleep
函数,很慢,非常慢,计时仍在继续。3秒到了,计时事件timeout
完成,task()
进入Event Queue,但是sleep
也太慢了吧,还没执行完,只好等着。sleep
终于执行完了,task()
终于从Event Queue进入了主线程执行。
setTimeout(fn,0)
当setTimeout的延时为0时,是不是意味着他会立即执行呢?
答案是:NO!
setTimeout(fn,0)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行
关于setTimeout
要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。
setInterval
setTimeout表示执行一个定时函数,每经过单位时间,就执行一次回调函数
上面说完了setTimeout
,当然不能错过它的孪生兄弟setInterval
。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval
会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)
来说,我们已经知道不是每过ms
秒会执行一次fn
,而是每过ms
秒,会有fn
进入Event Queue。一旦**setInterval
的回调函数fn
执行时间超过了延迟时间ms
,那么就完全看不出来有时间间隔了**。
宏任务(macro-task)和微任务(micro-task)
除了广义上的同步任务和异步任务,我们对任务有更精细的定义:
macro-task(宏任务):包括整体代码script,setTimeout\setInterval…micro-task(微任务):Promise,Process.nextTick…
核心知识点伪代码;
for (const macroTask of macroTaskQueue) {//先执行宏任务 handleMacroTask(); for (const microTask of microTaskQueue) {//再执行宏任务中的微任务 handleMicroTask(microTask); }}
事件循环,宏任务,微任务的关系图:
用一段代码来说明:
console.log('1');setTimeout(function() {console.log('2'); process.nextTick(function() {console.log('3'); }) new Promise(function(resolve) {console.log('4'); resolve(); }).then(function() {console.log('5') })})process.nextTick(function() {console.log('6');})new Promise(function(resolve) {console.log('7'); resolve();}).then(function() {console.log('8')})setTimeout(function() {console.log('9'); process.nextTick(function() {console.log('10'); }) new Promise(function(resolve) {console.log('11'); resolve(); }).then(function() {console.log('12') })})
第一轮事件循环流程分析如下:
整体script作为第一个宏任务进入主线程,遇到console.log
,输出1。遇到setTimeout
,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
。遇到process.nextTick()
,其回调函数被分发到微任务Event Queue中。我们记为process1
。遇到Promise
,new Promise
直接执行,输出7。then
被分发到微任务Event Queue中。我们记为then1
。又遇到了setTimeout
,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
。上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。我们发现了process1
和then1
两个微任务。执行process1
,输出6。执行then1
,输出8。
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1
宏任务开始:
首先输出2。接下来遇到了process.nextTick()
,同样将其分发到微任务Event Queue中,记为process2
。new Promise
立即执行输出4,then
也分发到微任务Event Queue中,记为then2
。第二轮事件循环宏任务结束,我们发现有process2
和then2
两个微任务可以执行。输出3。输出5。第二轮事件循环结束,第二轮输出2,4,3,5。第三轮事件循环开始,此时只剩setTimeout2了,执行。直接输出9。将process.nextTick()
分发到微任务Event Queue中。记为process3
。直接执行new Promise
,输出11。将then
分发到微任务Event Queue中,记为then3
。第三轮事件循环宏任务执行结束,执行两个微任务process3
和then3
。输出10。输出12。第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。 (请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
?如何实现一个事件的订阅与发布
7.函数式编程
什么是函数式编程?
函数式编程(Functional Programming,后面简称FP),维基百科的定义是:
是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
我来尝试理解下这个定义,好像就是说,在敲代码的时候,我要把过程逻辑写成函数,定义好输入参数,只关心它的输出结果。而且可以把函数作为输入输出。感觉好像平常写js时,就是这样的嘛!
特性:
函数是一等公民。就是说函数可以跟其他变量一样,可以作为其他函数的输入输出。喔,回调函数就是典型应用。不可变量。就是说,不能用var跟let咯。按这要求,我似乎有点难写代码。纯函数。就是没有副作用的函数。这个好理解,就是不修改函数外部的变量。引用透明。这个也好理解,就是说同样的输入,必定是同样的输出。函数内部不依赖外部状态,如一些全局变量。惰性计算。大意就是:一个表达式绑定的变量,不是声明的时候就计算出来,而是真正用到它的时候才去计算。
函数式编程的优劣势
优势
更好的管理状态。因为它的宗旨是无状态,或者说更少的状态。而平常DOM的开发中,因为DOM的视觉呈现依托于状态变化,所以不可避免的产生了非常多的状态,而且不同组件可能还相互依赖。以FP来编程,能最大化的减少这些未知、优化代码、减少出错情况。更简单的复用。极端的FP代码应该是每一行代码都是一个函数,当然我们不需要这么极端。我们尽量的把过程逻辑以更纯的函数来实现,固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响。更优雅的组合。往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。参考上面第二点,更强的复用性,带来更强大的组合性。隐性好处。减少代码量,提高维护性。
劣势
JavaScript不能算是严格意义上的函数式语言,很多函数式编程的特性并没有。比如上文说的数组的惰性链求值。为了实现它就得上工具库,或者自己封装实现,提高了代码编写成本。跟过程式相比,它并没有提高性能。有些地方,如果强制用FP去写,由于没有中间变量,还可能会降低性能。代码不易读。这个因人而异,因码而已。特别熟悉FP的人可能会觉得这段代码一目了然。而不熟悉的人,遇到写的晦涩的代码,看着一堆堆lambda演算跟匿名函数() => () => ()
瞬间就懵逼了。看懂代码,得脑子里先演算半小时。学习成本高。一方面继承于上一点。另一方面,很多前端coder,就是因为相对不喜欢一些底层的抽象的编程语言,才来踏入前端坑,你现在又让他们一头扎入FP,显得手足无措。
函数柯里化(curry)
curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
函数柯里化是指一个函数接收参数但不执行,知道所有参数都接到之后再执行
实现一个curry函数
function currying(fn,...args){const length = fn.length let argArray = [...args] const res = function(...args2){argArray = [...argArray,...args2] //长度相等或者大于fn所需参数就返回执行结果 if(argArray.length >= length){ return fn(...argArray) }else{ //长度不相等继续返回函数 return res } } return res }const add = (a,b,c) => a+b+c const a = currying(add,1) console.log(a(2,3));//output: 6
8.Service Worker/PWA
9.Web Worker
10.ES6知识
ES6所增加的知识太多了,给一个传送口
1.5万字概括ES6全部特性(已更新ES) - 掘金 ()
2.CSS基础
1.position
什么是position?
CSS **position
**属性用于指定一个元素在文档中的定位方式。top
,right
,bottom
和left
属性则决定了该元素的最终位置。
position的属性
static:表示元素按文档流中本应该放置的位置来排版relative:表示相对定位absolute:表示绝对定位fixed:固定定位sticky:表示粘性定位
relative
特性:
不脱离文档流默认参照物为此元素原位置
absolute
特性:
脱离文档流默认参照物为浏览器视窗的左上角
绝对定位元素相对于最近的非static
祖先元素定位。当这样的祖先元素不存在时,则相对于ICB(inital container block, 初始包含块)。
可以查看示例
A Pen by 张振明 (codepen.io)
fixed
特性
脱离文档流默认参照物为浏览器视窗位置
sticky(粘性定位)
单词sticky的中文意思是“粘性的”,position:sticky
表现也符合这个粘性的表现。基本上,可以看出是position:relative
和position:fixed
的结合体——当元素在屏幕内,表现为relative,就要滚出显示器屏幕的时候,表现为fixed。
特性:
当元素没有到达指定的位置时,position为relative当元素到达指定的位置,position为fixed,也就是固定住sticky元素的父级元素不能有任何overflow:visible
以外的overflow设置,否则没有粘滞效果
2.行内元素/块级元素
什么是行内元素?
行内元素只占据它对应标签的边框所包含的空间
一般情况下,行内元素只能包含数据和其他行内元素。
行内元素列表:
b, big, i, small, ttabbr, acronym, cite, code, dfn, em, kbd, strong, samp, vara, bdo, br, img, map, object, q, script, span, sub, supbutton, input, label, select, textarea
什么是块级元素
块级元素占据其父元素(容器)的整个水平空间,垂直空间等于其内容高度,因此创建了一个“块”
通常浏览器会在块级元素前后另起一个新行
行内元素与块级元素的区别
块级元素与行内元素有几个关键区别:
格式
默认情况下,块级元素会新起一行。
内容模型
一般块级元素可以包含行内元素和其他块级元素。这种结构上的包含继承区别可以使块级元素创建比行内元素更”大型“的结构。
HTML 标准中块级元素和行内元素的区别至高出现在 4.01 标准中。在 HTML5,这种区别被一个更复杂的内容类别 (en-US)代替。”块级“类别大致相当于 HTML5 中的流内容 (en-US)类别,而”行内“类别相当于 HTML5 中的措辞内容 (en-US)类别,不过除了这两个还有其他类别。
BFC(块格式化上下文)
块格式化上下文(Block Formatting Context,BFC)是Web页面的可视CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。
下列方式会创建块格式化上下文:
根元素(<html>)
浮动元素(元素的float
不是none
)绝对定位元素(元素的position
为absolute
或fixed
)行内块元素(元素的display
为inline-block
)表格单元格(元素的display
为table-cell
,HTML表格单元格默认为该值)表格标题(元素的display
为table-caption
,HTML表格标题默认为该值)匿名表格单元格元素(元素的display
为table、``table-row
、table-row-group、``table-header-group、``table-footer-group
(分别是HTML table、row、tbody、thead、tfoot 的默认属性)或inline-table
)overflow
计算值(Computed)不为visible
的块元素display
值为flow-root
的元素contain
值为layout
、content
或 paint 的元素弹性元素(display
为flex
或inline-flex
元素的直接子元素)网格元素(display
为grid
或inline-grid
元素的直接子元素)多列容器(元素的column-count
或column-width
(en-US) 不为auto,包括 ``column-count
为1
)column-span
为all
的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)。
块格式化上下文包含创建它的元素内部的所有内容.
块格式化上下文对浮动定位(参见float
)与清除浮动(参见clear
)都很重要。浮动定位和清除浮动时只会应用于同一个BFC内的元素。浮动不会影响其它BFC中元素的布局,而清除浮动只能清除同一BFC中在它前面的元素的浮动。外边距折叠(Margin collapsing)也只会发生在属于同一BFC的块级元素之间。
3.flex布局
什么是flex布局?
顾名思义,flex布局就是弹性布局,在flex容器中默认存在两条轴,水平主轴(main axis)和垂直的交叉轴(cross axis),我们可以通过设置属性将垂直方向变为主轴,水平方向变为交叉轴
在容器内的每个单元块被称为flex item,每个项目占据的主轴空间为(main size),占据的交叉轴的空间为(cross size)
flex容器
实现 flex 布局需要先指定一个容器,任何一个容器都可以被指定为 flex 布局,这样容器内部的元素就可以使用 flex 来进行布局。
.container {display: flex | inline-flex; //可以有两种取值}
需要注意的是:当时设置 flex 布局之后,子元素的 float、clear、vertical-align 的属性将会失效。
有下面六种属性可以设置在容器上,它们分别是:
flex-directionflex-wrapflex-flowjustify-contentalign-itemsalign-content
flex-direction:决定主轴的方向
.container {flex-direction: row | row-reverse | column | column-reverse;}
row:默认值,主轴为水平方向,起点在左端
row-reverse:主轴为水平方向,起点在右端
column:主轴为垂直方向,起点在上沿
column-reverse:主轴为垂直方向,起点在下端
flex-wrap:决定容器内项目是否可以换行
默认情况下,项目都排在主轴线上,使用 flex-wrap 可实现项目的换行。
.container {flex-wrap: nowrap | wrap | wrap-reverse;}
nowrap:默认值,不换行,即当主轴尺寸固定时,当空间不足时,项目尺寸会随之调整而并不会挤到下一行。
wrap:换行,当项目主轴总尺寸超过容器时换行,第一行在上方
wrap-reverse:换行,第一行在下方
flex-flow:flex-direction 和 flex-wrap 的简写形式
.container {flex-flow: <flex-direction> || <flex-wrap>;}
默认值为: row nowrap
justify-content:定义了项目在主轴的对齐方式。
.container {justify-content: flex-start | flex-end | center | space-between | space-around;}
假设主轴是水平方向,垂直方向同理(左右改为上下)
flex-start:左对齐
flex-end:右对齐
center:居中
space-between:两端对齐,项目之间的间隔相等,即剩余空间等分成间隙。
space-around:每个项目两侧的间隔相等,所以项目之间的间隔比项目与边缘的间隔大一倍。
align-items: 定义了项目在交叉轴上的对齐方式
.container {align-items: flex-start | flex-end | center | baseline | stretch;}
baseline:项目的第一行文字的基线对齐
stretch:默认值即如果项目未设置高度或者设为 auto,将占满整个容器的高度
align-content: 定义了多根轴线的对齐方式,如果项目只有一根轴线,那么该属性将不起作用
.container {align-content: flex-start | flex-end | center | space-between | space-around | stretch;}
当你 flex-wrap 设置为nowrap的时候,容器仅存在一根轴线,因为项目不会换行,就不会产生多条轴线。
当你 flex-wrap 设置为wrap的时候,容器可能会出现多条轴线,这时候你就需要去设置多条轴线之间的对齐方式了。
flex元素属性
有六种属性可运用在 item 项目上:
orderflex-basisflex-growflex-shrinkflexalign-self
order: 定义项目在容器中的排列顺序,数值越小,排列越靠前,默认值为 0
.item {order: <integer>;}
flex-basis: 定义了在分配多余空间之前,项目占据的主轴空间,浏览器根据这个属性,计算主轴是否有多余空间
.item {flex-basis: <length> | auto;}
默认值:auto,即项目本来的大小, 这时候 item 的宽高取决于 width 或 height 的值。
当主轴为水平方向的时候,当设置了 flex-basis,项目的宽度设置值会失效,flex-basis 需要跟 flex-grow 和 flex-shrink 配合使用才能发挥效果。
当 flex-basis 值为 0 % 时,是把该项目视为零尺寸的,故即使声明该尺寸为 140px,也并没有什么用。当 flex-basis 值为 auto 时,则跟根据尺寸的设定值(假如为 100px),则这 100px 不会纳入剩余空间。
flex-grow: 定义项目的放大比例
.item {flex-grow: <number>;}
默认值为 0,即如果存在剩余空间,也不放大
当所有的项目都以 flex-basis 的值进行排列后,仍有剩余空间,那么这时候 flex-grow 就会发挥作用了。
如果所有项目的 flex-grow 属性都为 1,则它们将等分剩余空间。(如果有的话)
如果一个项目的 flex-grow 属性为 2,其他项目都为 1,则前者占据的剩余空间将比其他项多一倍。
当然如果当所有项目以 flex-basis 的值排列完后发现空间不够了,且 flex-wrap:nowrap 时,此时 flex-grow 则不起作用了,这时候就需要接下来的这个属性。
flex-shrink: 定义了项目的缩小比例
.item {flex-shrink: <number>;}
默认值: 1,即如果空间不足,该项目将缩小,负值对该属性无效。
这里可以看出,虽然每个项目都设置了宽度为 50px,但是由于自身容器宽度只有 200px,这时候每个项目会被同比例进行缩小,因为默认值为 1。
同理可得:
如果所有项目的 flex-shrink 属性都为 1,当空间不足时,都将等比例缩小。
如果一个项目的 flex-shrink 属性为 0,其他项目都为 1,则空间不足时,前者不缩小。
flex: flex-grow, flex-shrink 和 flex-basis的简写
flex 的默认值是以上三个属性值的组合。假设以上三个属性同样取默认值,则flex 的默认值是 0 1 auto。
grow 和 shrink 是一对双胞胎,grow 表示伸张因子,shrink 表示是收缩因子。
grow 在 flex 容器下的子元素的宽度和比容器和小的时候起作用。 grow 定义了子元素的尺寸增长因子,容器中除去子元素之和剩下的尺寸会按照各个子元素的 grow 值进行平分加大各个子元素上。
lign-self: 允许单个项目有与其他项目不一样的对齐方式
单个项目覆盖 align-items 定义的属性
默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。
.item {align-self: auto | flex-start | flex-end | center | baseline | stretch;}
这个跟 align-items 属性时一样的,只不过 align-self 是对单个项目生效的,而 align-items 则是对容器下的所有项目生效的。
A Pen by 张振明 (codepen.io)
如何用flex实现九宫格布局
flex实现九宫格布局 (codepen.io)
flex:1指的是什么?flex属性默认值是什么
flex:1
为:flex: 1 1 0
;
flex属性默认值为:0 1 auto
具体代表什么属性 上文有说明
介绍一下flex-shrink和flex-basis属性
4.1px问题
5.重绘与回流
参考:[浏览器的重绘和回流](浏览器的回流与重绘 (Reflow & Repaint) - 掘金 ())
在讨论回流与重绘之前,我们要知道:
浏览器使用流式布局模型 (Flow Based Layout)。浏览器会把HTML
解析成DOM
,把CSS
解析成CSSOM
,DOM
和CSSOM
合并就产生了Render Tree
。有了RenderTree
,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。由于浏览器使用流式布局,对Render Tree
的计算通常只需要遍历一次就可以完成,但table
及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table
布局的原因之一。
一句话:回流必将引起重绘,重绘不一定会引起回流。
1.回流 (Reflow)
当Render Tree
中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
页面首次渲染浏览器窗口大小发生改变元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)元素字体大小变化添加或者删除可见的DOM
元素激活CSS
伪类(例如::hover
)查询某些属性或调用某些方法
一些常用且会导致回流的属性和方法:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
2.重绘 (Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color
、background-color
、visibility
等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
3.性能影响
回流比重绘的代价要更高。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。
现代浏览器会对频繁的回流或重绘操作进行优化:
浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。
当你访问以下属性或方法时,浏览器会立刻清空队列:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
width
、height
getComputedStyle()
getBoundingClientRect()
因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。
4.如何避免
1.CSS
避免使用table
布局。尽可能在DOM
树的最末端改变class
。避免设置多层内联样式。将动画效果应用到position
属性为absolute
或fixed
的元素上。避免使用CSS
表达式(例如:calc()
)。2.JavaScript
避免频繁操作样式,最好一次性重写style
属性,或者将样式列表定义为class
并一次性更改class
属性。避免频繁操作DOM
,创建一个documentFragment
,在它上面应用所有DOM操作
,最后再把它添加到文档中。也可以先为元素设置display: none
,操作结束后再把它显示出来。因为在display
属性为none
的元素上进行的DOM
操作不会引发回流和重绘。避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。总结:会引起元素位置变化的就会reflow,如博主上面介绍的,窗口大小改变、字体大小改变、以及元素位置改变,都会引起周围的元素改变他们以前的位置;不会引起位置变化的,只是在以前的位置进行改变背景颜色等,只会repaint;
6.居中布局
7.层叠上下文
参考:彻底搞懂CSS层叠上下文、层叠等级、层叠顺序、z-index - 掘金 ()
首先,z-index
属性值并不是在任何元素上都有效果。它仅在定位元素(定义了position
属性,且属性值为非static
值的元素)上有效果。
判断元素在Z轴
上的堆叠顺序,不仅仅是直接比较两个元素的z-index
值的大小,这个堆叠顺序实际由元素的层叠上下文、层叠等级共同决定。
1.什么是层叠上下文
层叠上下文(stacking context),是HTML中一个三维的概念。在CSS2.1规范中,每个盒模型的位置是三维的,分别是平面画布上的X轴
,Y轴
以及表示层叠的Z轴
。一般情况下,元素在页面上沿X轴Y轴
平铺,我们察觉不到它们在Z轴
上的层叠关系。而一旦元素发生堆叠,这时就能发现某个元素可能覆盖了另一个元素或者被另一个元素覆盖。
如果一个元素含有层叠上下文,(也就是说它是层叠上下文元素),我们可以理解为这个元素在Z轴
上就“高人一等”,最终表现就是它离屏幕观察者更近。
具象的比喻:你可以把层叠上下文元素理解为理解为该元素当了官,而其他非层叠上下文元素则可以理解为普通群众。凡是“当了官的元素”就比普通元素等级要高,也就是说元素在
Z轴
上更靠上,更靠近观察者。
2.什么是层叠等级
那么,层叠等级指的又是什么?层叠等级(stacking level,叫“层叠级别”/“层叠水平”也行)
在同一个层叠上下文中,它描述定义的是该层叠上下文中的层叠上下文元素在Z轴
上的上下顺序。在其他普通元素中,它描述定义的是这些普通元素在Z轴
上的上下顺序。
说到这,可能很多人疑问了,不论在层叠上下文中还是在普通元素中,层叠等级都表示元素在Z轴
上的上下顺序,那就直接说它描述定义了所有元素在Z轴
上的上下顺序就OK啊!为什么要分开描述?
为了说明原因,先举个栗子:
具象的比喻:我们之前说到,处于层叠上下文中的元素,就像是元素当了官,等级自然比普通元素高。再想象一下,假设一个官员A是个省级领导,他下属有一个秘书a-1,家里有一个保姆a-2。另一个官员B是一个县级领导,他下属有一个秘书b-1,家里有一个保姆b-2。a-1和b-1虽然都是秘书,但是你想一个省级领导的秘书和一个县级领导的秘书之间有可比性么?甚至保姆a-2都要比秘书b-1的等级高得多。谁大谁小,谁高谁低一目了然,所以根本没有比较的意义。只有在A下属的a-1、a-2以及B下属的b-1、b-2中相互比较大小高低才有意义。
再类比回“层叠上下文”和“层叠等级”,就得出一个结论:
普通元素的层叠等级优先由其所在的层叠上下文决定。层叠等级的比较只有在当前层叠上下文元素中才有意义。不同层叠上下文中比较层叠等级是没有意义的。
3.如何产生“层叠上下文”
前面说了那么多,知道了“层叠上下文”和“层叠等级”,其中还有一个最关键的问题:到底如何产生层叠上下文呢?如何让一个元素变成层叠上下文元素呢?
其实,层叠上下文也基本上是有一些特定的CSS属性创建的,一般有3种方法:
HTML
中的根元素<html></html>
本身j就具有层叠上下文,称为“根层叠上下文”。普通元素设置position
属性为非static
值并设置z-index
属性为具体数值,产生层叠上下文。CSS3中的新属性也可以产生层叠上下文。
上面说了那么多,可能你还是有点懵。这么多概念规则,来点最实际的,有没有一个“套路”当遇到元素层叠时,能很清晰地判断出他们谁在上谁在下呢?答案是——肯定有啊!
首先先看要比较的两个元素是否处于同一个层叠上下文中 如果是,谁的层叠等级大,谁在上面如果两个元素不在统一层叠上下文中,请先比较他们所处的层叠上下文的层叠等级 当两个元素层叠等级相同、层叠顺序相同时,在DOM结构中后面的元素层叠等级在前面元素之上
8.sass/less
3.Vue
1.MVVM
1.什么是MVVM?
Model–View–ViewModel (MVVM) 是一个软件架构设计模式,源于经典的Model-View-Controller(MVC)模式,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率.
MVVM的核心是ViewModel层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。
View层
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。
Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
ViewModel 层
ViewModel
是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。
2.ViewModel有什么好处?
MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。
2.生命周期
1.介绍一下Vue生命周期
Vue 实例有一个完整的生命周期,也就是从开始创建实例、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。
各个生命周期的作用
beforeCreate:此时生命周期以及事件已经被初始化但是数据代理还未开始,无法通过vm访问到data中的数据、methods中的方法
created:此时数据监测和数据代理已经初始化,可以通过访问vm访问到data中的数据
beforeMount:此阶段Vue开始解析模板,生成虚拟DOM(内存中),但是页面还不能显示解析好的内容,
此时页面呈现的时未经Vue编译的DOM结构,所有对DOM的操作,最终都不奏效
mouted:此阶段内存中的虚拟DOM已经转成真实DOM插入页面。页面中呈现的时经过Vue编译的DOM,此时对DOM的操作均有效。至此初始化过程结束,一般再次进行:开启定时器、发送网络请求、订阅消息、绑定自定义事件等初始化操作、
beforeUpdate:此时数据是新的,但是页面是旧的,页面尚未和数据保持同步
updated:此时页面已经完成了从Model到View的更新,页面数据是新的,页面也是新的,即页面和数据保持同步
beforeDestory:此时vm中所有的data、methods、指令等等都处于可用状态,但是马上要执行销毁过程,一般在此阶段:关闭定时器,取消订阅消息、解绑自定义事件等收尾操作
destoryed:销毁vm实例
2.nextTick是如何实现的
什么是nextTick?
nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用nextTick,则可以在回调中获取更新后的 DOM
nextTick原理
3.父子组件挂载时,生命周期的顺序是怎么样的
vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程
父 beforeUpdate -> 父 updated
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
3.数据绑定
1.Vue的双向绑定如何实现
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
1、需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
2、compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
3、Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
4、MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
按我自己的理解就是:当修改数据时,vue会调用dep.notify通知相应的wathcer执行它的update函数,update函数会执行compile中绑定的回调,然会修改dom的值.当修改视图层的数据时,vue通过监听input来获取input中的值并将其赋值给data中的对应属性,修改属性又会触发setter,于是又会执行dep.notify等一系列操作,从而达到双向绑定.
2.Vue如何监听数组或对象的改变
1.vue会监视data中所有层次的数据
2.如何监测对象中的数据?
通过setter事件监视,且要在newVue时就传入要监测的数据
(1).在对象后追加的属性,Vue默认不做响应式处理
(2).如需给后添加的属性做响应式,需要使用以下API:
Vue.set(target,propertyName/index,value)
vm.$set(target,propertyName/index,value)
3.如何监测数组中的数据?
通过包裹数组更新元素的方法实现,本质就是做了两件事:
(1).调用原生对应的方法对数组进行更新
(2).重新解析模板,进而更新页面
4.在Vue修改数组中的某个元素需要用到以下方法:
1.使用这些API:push(),pop(),shift(),unshift(),splice(),sort(),erverse()
2.Vue.set()或vm.$set
3.defineProperty和proxy的区别
Proxy 的优势如下:
Proxy 可以直接监听对象而非属性;Proxy 可以直接监听数组的变化;Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平
4.Vue中的数据为什么频繁变化但只会更新一次
4.状态管理
1.vuex是什么
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。专门在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信
2.什么时候使用vuex?
多个组件依赖同一状态 来自不同组件的行为需要变更同一状态3.vuex的工作原理
vuex包括以下几个模块:
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
1.vuex管理的状态对象(存放数据的对象)
2.它应该时唯一的
3.示例代码:
const state = {// key:value}
Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
当state中的数据需要经过加工后再使用时,可以使用getters加工,类似于组件中的计算属性
Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
1.值是一个对象,包含多个直接更新state的方法
2.谁能调用mutations中的方法?如何让调用?
在action中使用:**commit(‘对应的mutations方法名’)**触发
3.mutations中方法的特点:不能写异步代码、只能单纯的操作state
4.示例代码:
const mutations = {//函数}
Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
1.值为一个对象,包含多个响应用户动作的回调函数
2.通过commit()来触发mutation中函数的调用,间接更新state
3.示例代码:
const actions = {//函数}
Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
4.vuex中4个map方法的使用
定义:使用vuex提供的map方法可以对组件中的计算属性和方法进行映射,可以大大的减少代码量,提高代码的复用率
1.mapState
理解:用于帮助我们映射state中的数据为计算属性
computed:{//借助mapState生成计算属性:count,student,school(对象写法)//对象中的key是我们在自己的组件中渲染时需要调用的值,value是我们在store中state内定义的值...mapState({count:'count',student:'student',school:'school'}),//借助mapState生成计算属性:count,student,school(数组写法)...mapState(['count','student','school']),},
2.mapGetters
理解:用于帮助我们映射getters中的数据为计算属性
computed:{//借助mapGetters生成计算属性:bigCount(对象写法)//对象中的key是我们在自己的组件中渲染时需要调用的值,value是我们在store中getters内定义的值...mapState({count:'count',student:'student',school:'school'}),//借助mapGetters生成计算属性:bigCount(数组写法)...mapGetters(['bigCount'])},
3.mapActions
理解:用于帮助我们生成与action对话的方法,即:包含$store.dispatch(xxx)的函数
methods:{//借助mapActions生成方法:addOdd,addWait(对象写法)//对象中的key是我们在自己的组件中需要调用的方法,value是我们在store中actions内定义的方法名...mapActions({addOdd:'addOdd',addWait:'addWait'}),//借助mapActions生成方法:addOdd,addWait(数组写法)...mapActions(['addOdd','addWait'])}
4.mapMutations
理解:用于帮助我们生成与mutations对话的方法,即:包含$mit(xxx)的函数
methods:{//借助mapActions生成方法:Add,Reduce(对象写法)//对象中的key是我们在自己的组件中需要调用的方法,value是我们在store中mutations内定义的方法名...mapMutations({add:'Add',reduce:'Reduce'}),//借助mapActions生成方法:Add,Reduce(数组写法)...mapActions(['Add','Reduce'])}
注意:当我们使用mapActions和mapMutations方法时,我们需要将dispatch和commit中需要传递的值通过组件中方法传参,例如:
<button @click="add(n)">+</button>
5.vuex的基本使用
案例:点击按钮对数字进行各种操作
我们将求和之后的数字定义为count,存入vuex的临时组件中,每次进行操作从原组件中调用vuex的api,最终完成求和的计算
Count.vue组件:
<div ><h1>当前求和为:{{$store.state.count}}</h1><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add">+</button><button @click="reduce">-</button><button @click="addOdd">当前求和为奇数再加</button><button @click="addWait">等一等再加</button></div>
export default {data(){return{n:1,//选择框中选择的数字}},methods:{add(){this.$mit('Add',this.n)},reduce(){this.$mit('Reduce',this.n)},addOdd(){this.$store.dispatch('addOdd',this.n)},addWait(){this.$store.dispatch('addWait',this.n)}}}
store中index.js
//该文件用于创建Vuex中最核心的storeimport Vue from 'vue'//引入Vueximport Vuex from 'vuex'Vue.use(Vuex)//准备actions--用于响应组件中的动作const actions = {//action中可以进行异步操作,例如从请求服务器接口addOdd(context,value){if(context.state.count % 2){mit('AddOdd',value) }},addWait(context,value){setTimeout(()=>{mit('AddWait',value) },1000)},}//准备mutations--用于操作数据(state)const mutations = {Add(state,value){state.count += value},Reduce(state,value){state.count -= value},AddOdd(state,value){state.count += value},AddWait(state,value){state.count += value}}//准备state--用于存储数据const state = {count:1 }//创建并暴露storeexport default new Vuex.Store({actions,mutations,state})
效果
6.vuex中的数据在页面刷新后消失怎么办?
用sessionstorage 或者 localstorage 存储数据
存储: sessionStorage.setItem( '名', JSON.stringify(值) )使用: sessionStorage.getItem('名') ---得到的值为字符串类型,用JSON.parse()去引号;
5.组件通信
1.vue组件间通信有哪几种方式?
1.props/$emit
适用:父子组件通信
父传子:父组件通过往子组件标签中添加需要传递的数据,子组件使用props来接收,prop只读,不可修改,即单向数据流
子传父:$emit
绑定一个自定义事件, 当这个语句被执行时, 就会将参数arg传递给父组件,父组件通过v-on监听并接收参数
2.ref
适用:父子组件通信
如果ref挂载在普通的DOM元素上,引用指向的就是DOM元素;如果挂载在子组件上,引用就指向组件实例
父组件可以通过this.$ref.xxx来获取子组件实例
3.parent/parent/parent/children
适用于:父子组件通信
通过this.parent∗∗和∗∗this.parent**和**this.parent∗∗和∗∗this.children来获取对应的父子组件实例
4.EventBus
适用于:任意组件间通信
这种方法通过一个空的Vue实例作为中央事件总线,用它来触发事件和监听事件,从而实现任意组件之间的通信
发布事件:通过$emit来发布事件
订阅事件:通过$on来订阅事件,当监听到发布的事件后,执行相应的回调
//组件1 发布事件new Vue({data:{},methods:{emit(){this.bus.$emit.sayhello('say','hello')//发布事件}}})//组件2 订阅事件new Vue({data:{},mounted(){this.bus.$on('say',this.say)//订阅事件 },methods:{say(word){console.log(word);}}})
eventBus也有不方便之处, 当项目较大,就容易造成难以维护的灾难
5.attrs/attrs/attrs/listeners
适用于:隔代组件通信
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过v-bind="$attrs"
传入内部组件。通常配合 inheritAttrs 选项一起使用。
$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过v-on="$listeners"
传入内部组件
6.provide/inject
适用于:隔代组件通信
provide
/inject
是vue2.2.0
新增的api, 简单来说就是父组件中通过provide
来提供变量, 然后再子组件中通过inject
来注入变量。
注意: 这里不论子组件嵌套有多深, 只要调用了
inject
那么就可以注入provide
中的数据,而不局限于只能从当前父组件的props属性中回去数据
7.vuex
适用于:任意组件间通信
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. Vuex 解决了多个视图依赖于同一状态
和来自不同视图的行为需要变更同一状态
的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上
Vuex的各个模块:
state
:用于数据的存储,是store中的唯一数据源getters
:如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算mutations
:类似函数,改变state数据的唯一途径,且不能用于处理异步事件actions
:类似于mutation
,用于提交mutation
来改变状态,而不直接变更状态,可以包含任意异步操作modules
:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护
8.localStorage/sessionStorage
通过浏览器缓存来实现组件间通信
6.Virtual DOM
1.虚拟dom是什么
虚拟DOM简而言之就是,用JS去按照DOM结构来实现的树形结构对象,你也可以叫做DOM对象
2.为什么需要虚拟dom
优点:
保证性能下限:框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;无需手动操作 DOM:我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;跨平台:虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点:
无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
3.vue的虚拟dom解决了什么问题
无需手动操纵dom虚拟dom可以方便的进行跨平台操作,例如在node环境中无法应用真实dom,但是可以使用虚拟dom4.虚拟DOM的实现原理
虚拟 DOM 的实现原理主要包括以下 3 部分:
用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;diff 算法 — 比较两棵虚拟 DOM 树的差异;pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
7.diff
1.实现diff的思路
用JS模拟真实DOM节点把虚拟DOM转换成真实DOM插入页面中发生变化时,比较两棵树的差异,生成差异对象根据差异对象更新真实DOM2.vue中的key的作用
1.key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据新数据生成新的虚拟DOM,随后Vue进行新虚拟DOM与旧虚拟DOM的差异比较
2.对比规则:
(1).旧虚拟DOM中找到了与新虚拟DOM相同的key:
①.若虚拟DOM中内容没有变化,直接使用之前的真实DOM
②.若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
(2).旧虚拟DOM中未找到与新虚拟DOM相同的key
创建新的真实DOM,随后渲染到页面
3.用index作为key可能会引发的问题:
(1).若对数据进行逆序添加、逆序删除等破坏顺序的操作会产生没有必要的真实DOM更新,界 面效果没有问题,但是执行效率很低
(2).如果结构中还包含输入类的DOM:会产生错误DOM更新,界面有问题
4.开发中如何选择key?
(1).最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值
(2).如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于展示,使用index作为key是没有问题的
index作为key和id作为key的区别
8.Vue computed/watch
puted 和 watch 的区别和运用的场景?
computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch:更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
9.Vue和React有什么不同
相同点:
1,都使用了Virtual DOM。
2,都提供了响应式和组件化的视图组件。
3,都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关库。
不同点:
1,React中,当某组件的状态发生改变时,它会以该组件为根,重新渲染整个组件子树,而在Vue中,组件的依赖是在渲染的过程中自动追踪的,所以系统能准确知晓哪个组件确实需要被重新渲染。
2,Vue的路由库和状态管理库都由官方维护支持且与核心库同步更新,而React选择把这些问题交给社区维护,因此生态更丰富。
3,Vue-cli脚手架可进行配置
4.网络
1.HTTP
2.DNS
3.TCP
4.HTTPS
5.CDN
6.从输入url到页面展示发生了什么
如果觉得《前端面试必备知识点总结(持续更新)》对你有帮助,请点赞、收藏,并留下你的观点哦!