主要名词
JS引擎 — 一个读取代码并运行的引擎,没有单一的“JS引擎”,每个浏览器都有自己的引擎,如谷歌有V8。
作用域 — 可以从中访问变量的“区域”。
词法作用域 — 在词法阶段的作用域,换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。
块作用域 — 由花括号{}创建的范围
作用域链 — 函数可以上升到它的外部环境(词法上)来搜索一个变量,它可以一直向上查找,直到它到达全局作用域。
同步 — 一次执行一件事, “同步”引擎一次只执行一行,JavaScript是同步的。
异步 — 同时做多个事,JS通过浏览器API模拟异步行为
事件循环(Event Loop) - 浏览器API完成函数调用的过程,将回调函数推送到回调队列(callback queue),然后当堆栈为空时,它将回调函数推送到调用堆栈。
堆栈 —一种数据结构,只能将元素推入并弹出顶部元素。 想想堆叠一个字形的塔楼; 你不能删除中间块,后进先出。
堆 — 变量存储在内存中。
调用堆栈 — 函数调用的队列,它实现了堆栈数据类型,这意味着一次可以运行一个函数。 调用函数将其推入堆栈并从函数返回将其弹出堆栈。
执行上下文 — 当函数放入到调用堆栈时由JS创建的环境。
闭包 — 当在另一个函数内创建一个函数时,它“记住”它在以后调用时创建的环境。
垃圾回收 — 当内存中的变量被自动删除时,因为它不再使用,引擎要处理掉它。
变量的提升 — 当变量内存没有赋值时会被提升到全局的顶部并设置为undefined。
this —由JavaScript为每个新的执行上下文自动创建的变量/关键字。
1、 执行上下文
1)、执行上下文介绍
JS代码在执行前,JS引擎要准备工作,这份工作其实就是创建对应的执行上下文;
js 有三种执行上下文类型:全局执行上下文,函数执行上下文,与eval函数执行上下文;
- 全局执行上下文:全局执行上下文只有一个,在客户端中一般由浏览器创建,创建一个全局的 window 对象,并且设置 this 的值等于这个全局对象。
- 函数执行上下文:函数执行上下文可存在无数个,每当一个函数被调用时都会创建一个函数上下文;需要注意的是,同一个函数被多次调用,都会创建一个新的上下文。
- eval函数执行上下文:执行在 eval 函数内部的代码也会有它属于自己的执行上下文。
2)、执行栈
执行上下文栈(下文简称执行栈)也叫调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。
JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;由于执行栈LIFO的特性,所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文。
代码理解:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
上述代码在被浏览器加载时,会先将全局执行环境压入栈底,当遇到 first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。second() 一样。最后当函数执行完后,依次出栈。
3)、创建执行上下文过程
创建执行上下文有两个阶段:1、 创建阶段;2、执行阶段。其中创建阶段是执行上下文的主要阶段。
创建阶段
JavaScript 执行上下文的创建阶段主要负责三件事:确定this---创建词法环境组件(LexicalEnvironment)---创建变量环境组件(VariableEnvironment)
-
this 值的决定,即我们所熟知的 This 绑定。
- 在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。
- 在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)。
-
词法环境组件。
词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
词法环境由环境记录与对外部环境引入记录两个部分组成。
- 环境记录器是存储变量和函数声明的实际位置。
- 外部环境的引用意味着它可以访问其父级词法环境(作用域的感觉)。
我们在前文提到了全局执行上下文与函数执行上下文,所以这也导致了词法环境分为全局词法环境与函数词法环境两种。
- 全局词法环境组件:对外部环境的引入记录为null,因为它本身就是最外层环境,除此之外它还记录了当前环境下的所有属性、方法位置。环境记录器是对象环境记录器。
- 函数词法环境组件:包含了用户在函数中定义的所有属性方法外,还包含了一个arguments对象和和传递给函数的参数的 length。函数词法环境的外部环境引入可以是全局环境,也可以是其它函数环境,这个根据实际代码而来。环境记录器是声明式环境记录器。
用伪代码表示:
// 全局环境 GlobalExectionContext = { // 全局词法环境 LexicalEnvironment: { // 环境记录 EnvironmentRecord: { Type: "Object", //类型为对象环境记录 // 标识符绑定在这里 }, outer: < null > } }; // 函数环境 FunctionExectionContext = { // 函数词法环境 LexicalEnvironment: { // 环境纪录 EnvironmentRecord: { Type: "Declarative", //类型为声明性环境记录 // 标识符绑定在这里 }, outer: < Global or outerfunction 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 = { // this绑定为全局对象 ThisBinding: <Global Object>, // 词法环境创建 LexicalEnvironment: { //环境记录 EnvironmentRecord: { Type: "Object", // 对象环境记录 // 标识符绑定在这里 let const创建的变量a b在这 a: < uninitialized >, b: < uninitialized >, multiply: < func > } // 全局环境外部环境引入为null outer: <null> }, //变量环境创建 VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 对象环境记录 // 标识符绑定在这里 var创建的c在这 c: undefined, } // 全局环境外部环境引入为null outer: <null> } } // 函数执行上下文 FunctionExectionContext = { //由于函数是默认调用 this绑定同样是全局对象 ThisBinding: <Global Object>, // 词法环境 LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 声明性环境记录 // 标识符绑定在这里 arguments对象在这 Arguments: {0: 20, 1: 30, length: 2}, }, // 外部环境引入记录为</Global> outer: <GlobalEnvironment> }, // 变量环境 VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 声明性环境记录 // 标识符绑定在这里 var创建的g在这 g: undefined }, // 外部环境引入记录为</Global> outer: <GlobalEnvironment> } }在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined,函数被设置为了自身函数,而let const被设置为未初始化。这是因为作用域创建阶段JS引擎对两者初始化赋值不同,这就是我们说的变量声明提升。
执行阶段
在此阶段,完成对所有这些变量的分配,最后执行代码。
在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined。
2、 作用域
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
1)、作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
简单描述函数上下文中作用域和变量对象的创建过程:
var scope = "global scope";
function f(){
var scope2 = 'local scope';
return scope2;
}
f();
执行过程如下:
-
f 函数被创建,保存作用域链到 内部属性[[scope]] (函数的作用域在函数定义的时候就决定)
f.[[scope]] = [ globalContext.VO ]; -
执行函数,函数确定执行上下文,函数执行上下文被压入执行上下文栈。(函数执行时确定上下文)
ECStack = [ fContext, globalContext ]; -
函数执行时,并不是立即执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
fContext = { Scope: f.[[scope]], } -
第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
fContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: f.[[scope]], } -
第三步:将活动对象压入 f 作用域链顶端
fContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] } -
准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
fContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]] } -
查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [ globalContext ];
3、 闭包
闭包是一个可以访问外部作用域中变量的内部函数。
这些被引用的变量直到闭包被销毁时才会被销毁。
闭包使得 timer 定时器,事件处理,AJAX 请求等异步任务更加容易。
可以通过闭包来达到封装性。
4、 this
在JavaScript中,this的指向是调用时决定的,而不是创建时决定的,这就会导致this的指向会让人迷惑,简单来说,this具有运行期绑定的特性。
1)、全局上下文
在全局执行上下文中this都指代全局对象。
- this等价于window对象
- var === this. === winodw.
2)、函数上下文
在函数内部,this的值取决于函数被调用的方式。
1. 直接调用
this指向全局变量。
function foo(){
return this;
}
console.log(foo() === window); // true
2. call()、apply()
this指向绑定的对象上。
var person = {
name: "风信子",
age: 25
};
function say(job){
console.log(this.name+":"+this.age+" "+job);
}
say.call(person,"FE"); // 风信子:25 FE
say.apply(person,["FE"]); // 风信子:25 FE
可以看到,定义了一个say函数是用来输出name、age和job,其中本身没有name和age属性,我们将这个函数绑定到person这个对象上,输出了本属于person的属性,说明此时this是指向对象person的。
如果传入一个原始值(字符串、布尔或数字类型)来当做this的绑定对象, 这个原始值会被转换成它的对象形式(new String()),这通常被称为“装箱”。
function say(job){
console.log(this+":"+job);
}
say.call("55222","FE"); // 55222:FE
say.apply(123,["FE"]); // 123:FE
call和apply从this的绑定角度上来说是一样的,唯一不同的是它们的第二个参数。
3. bind()
this将永久地被绑定到了bind的第一个参数。
bind和call、apply有些相似。
var person = {
name: "风信子",
age: 25
};
function say(){
console.log(this.name+":"+this.age);
}
var f = say.bind(person);
console.log(f()); // 风信子: 25
4. 箭头函数
所有的箭头函数都没有自己的this,都指向外层。
看下边的例子:
function foo() {
setTimeout(()=>{
console.log(this.a);
},100)
}
var obj = {
a: 2
}
foo.call(obj); // 2 this -> obj
foo 函数的 this 执行为对象 obj ,由于 箭头函数都没有自己的this,都指父外层this。所以箭头函数的为 obj
function foo2() {
setTimeout(function(){ // function函数
console.log(this.a);
},100)
}
var obj = {
a: 2
}
foo2(); // 6 this -> window
foo2.call(obj); // 6 this -> window
普通调用 foo2() , foo2指向 window ,function函数 被 setTimeout 直接调用 this 指向 window ;foo2 通过 call() 指向 obj,function函数 被 setTimeout 直接调用 this 指向 window
5. 作为对象的一个方法
this指向调用函数的对象。
var person = {
name: "风信子",
getName: function(){
return this.name;
}
}
console.log(person.getName()); // 风信子 this -> person
特别的:这里有一个需要注意的地方。。。
var name = "风信子";
var person = {
name: "风信子FE",
getName: function(){
return this.name;
}
}
var getName = person.getName;
console.log(getName()); // 风信子 this -> window
this的指向得看函数调用时。person.getName 直接调用 this 指向 person ,var getName = person.getName; 将函数赋值给变量 getName ,由 getName 直接调用函数, this 指向了 window
6. 作为一个构造函数
this被绑定到正在构造的新对象。
通过构造函数创建一个对象其实执行这样几个步骤:
- 创建新对象
- 将this指向这个对象
- 给对象赋值(属性、方法)
- 返回this
所以this就是指向创建的这个对象上。
function Person(name){
this.name = name;
this.age = 25;
this.say = function(){
console.log(this.name + ":" + this.age);
}
}
var person = new Person("风信子");
console.log(person.name); // 风信子 this -> Person
person.say(); // 风信子:25 this -> Person
7. 作为一个DOM事件处理函数
this指向触发事件的元素,也就是始事件处理程序所绑定到的DOM节点。
8. 总结
如果要判断一个函数的this绑定,就需要找到这个函数的直接调用位置。然后可以顺序按照下面四条规则来判断this的绑定对象:
- 由new调用:绑定到新创建的对象
- 由call或apply、bind调用:绑定到指定的对象
- 由上下文对象调用:绑定到上下文对象
- 默认:全局对象
注意:箭头函数不使用上面的绑定规则,根据外层作用域来决定this,继承外层函数调用的this绑定。
3)、call、apply 和 bind 的区别
- 三者都是用来改变函数的this指向
- 三者的第一个参数都是this指向的对象
- bind是返回一个绑定函数可稍后执行,call、apply是立即调用
- 三者都可以给定参数传递
- call给定参数需要将参数全部列出,apply给定参数数组
5、 原型
在了解原型之前首先了解一下规则以及以一下名词:
原型规则
- 所有的引用类型(数组、对象、函数)都具有对象特性,即可自由扩展属性(除了"null")
- 所有的引用类型(数组、对象、函数)都有一个__proto_(隐式原型)_属性,属性值是一个普通的对象
- 所有的函数(只有函数才具有显示原型),都有一个prototype(显示原型)属性,属性值也是一个普通的对象
- 所有的引用类型(数组、对象、函数),
__proto_(隐式原型)属性值 指向它构造函数的prototype(显示原型)属性值 - 当视图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数prototype)中寻找
普通对象和函数对象
JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。
var o1 = {};
var o2 =new Object();
var o3 = new f1();
function f1(){};
var f2 = function(){};
var f3 = new Function('str','console.log(str)');
console.log(typeof Object); //function
console.log(typeof Function); //function
console.log(typeof f1); //function
console.log(typeof f2); //function
console.log(typeof f3); //function
console.log(typeof o1); //object
console.log(typeof o2); //object
console.log(typeof o3); //object
在上面的例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。
构造函数
在 JavaScript 里,构造函数通常是用来实现实例的,JavaScript 没有类的概念,但是有特殊的构造函数。构造函数本质上是个普通函数,充当类的角色,主要用来创建实例,并初始化实例,即为实例成员变量赋初始值。 构造函数和普通函数的区别在于,构造函数应该遵循以下几点规范:
- 在命名上,构造函数首字母需要大写;
- 调用方式不同,普通函数是直接调用,而构造函数需要使用 new 关键字来进行调用;
- 在构造函数内部,this 指向的是新创建的实例;
- 构造函数中没有显示的 return 表达式,一般情况下,会隐式地返回 this,也就是新创建的对象,如果想要使用显示的返回值,则显示返回值必须是对象,否则依然返回实例。
实例
构造函数通过 new 出来的实例(对象)
关键字 new 的作用
- 创建一个空对象,作为将要返回的对象实例
- 将这个空的对象原型对象,指向了构造函数的prototype属性对象
- 将这个实例对象的值赋值给函数内部的this关键字
- 执行构造函数内的代码。
- 如果该函数没有返回对象,则返回this。
原型
解释: 原型是首创的模型,代表同一类型的人物、物件、或观念(维基百科)。那边对应到javascript 中来, 我们可以理解为:原型是对象创建之初的的模型,拥有同一类对象的公有属性和行为(方法)。
原型对象( prototype )
在js中每个函数都有一个指向原型的属性——prototype,称为原型对象。原型对象中有一个指向构造函数的的属性——constructor。这里的函数不光包括构造函数,也包括普通函数。
- 原型对象拥有公有的属性和方法
- js 继承是基于原型对象的
原型链(__proto__)
在js 中每个对象(null 除外)都有一个属性__proto__, 该属性指向原型对象,换句话说则是:该对象是从那个原型对象继承属性和方法,我们可以通过此属性来找到,原型链则是基于__proto__来实现的。
当我们访问一个对象的属性或者方法的时候,会先去对象自身找这个属性或者方法,如果没有找到,则会去它的原型对象身上找,如果原型对象身上也没有找到,则会到原型对象的原型对象上去找,以此类推,找到则返回对应的值,如果直到原型对象为null任然没有找到则返回undefined。
!
constructor
每个对象在创建时都会自动拥有一个构造函数constructor属性
构造函数、原型、实例之间的关系
- 这里的 function Person(){} 就是构造函数
- 构造函数 new 出来的 person就是它的实例(对象)
- 构造函数都有一个 prototype 属性,指向另一个对象(就是它的原型(对象)),这个对象的所有属性和方法,都会被构造函数的实例继承——就是说构造函数的原型和构造函数的实例 属性和方法可以共享
- 这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上
- 实例上有个属性__proto__ (双下划线) 指向原型
构造函数和原型的关系:
每个构造函数都有一个 prototype 属性,指向另一个对象(就是它的原型),用白话讲就是每写一个构造函数 就会同时生成个原型(没有构造函数中的属性或者方法,可以理解为空对象) 通过 构造函数.prototype可以设置和访问原型中的属性和方法 同时原型中也有个constructor属性可以指向构造函数
构造函数和实例的关系:
用 new 关键字创建 Person 实例时,内部执行了4个操作:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象 所以就把构造函数中的指针this 指向了新创建的实例中 实例中就可以调用构造函数中的属性和方法了
实例和原型的关系:
实例上有个属性__proto__ (双下划线) 指向原型
person.__proto__指向的就是构造函数Person的原型,即: person.__proto__ === Person.prototype
实例继承原型上的属性和方法
完整的原型链
-
f1、f2是由构造函数 Foo 实例的对象(普通对象)
f1.__proto__ === Foo.prototype -
Foo.prototype作为对象,__proto__指向Object.prototype
Foo.prototype.__proto__ === Object.prototype -
Foo.prototype有一个隐藏属性constructor,指向关联的构造函数,也就是Foo
Foo.prototype.constructor === Foo -
Foo为函数,亦是对象,所以Foo也有__proto__属性,指向Function.prototype
Foo.__proto__ === Function.prototype -
Function本身也是对象,也具有__proto__,指向了Function.prototype
Function.__proto__ === Function.prototype -
Function.prototype的constructor属性指向了Function
Function.prototype.constructor === Function -
Function.prototype同为对象具有__proto__属性,指向了Object.prototype
Function.prototype.__proto__ === Object.prototype -
Object既为函数,亦是对象,__proto__指向Function.prototype
Object.__proto__ === Function.prototype -
Object.prototype的隐藏属性constructor指向function Object()
Object.prototype.constructor === Object -
最终指向null
Object.prototype.__proto__
总结:
- __proto__指向prototype
- 函数对象(即函数)__ptoto__指向Function.prototype,Function、Object均是函数
- 原型链最终会找到Object.prototype,最终为null
6、 继承
1. 面向对象编程的继承方式
www.liuxiuqian.com/bloginfo/30
2. JS继承 原型链继承、构造函数继承、组合继承、原型继承、寄生式继承、寄生组合继承
7、 Promise
Promise 是异步编程的一种解决方案: 从语法上讲,promise是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。 promise有三种状态:pending(等待态),Fulfilled(成功态),rejected(失败态);状态一旦改变,就不会再变。创造promise实例后,它会立即执行。
promise是用来解决两个问题的:
- 回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象
- promise可以支持多个并发的请求,获取并发请求中的数据
- 这个promise可以解决异步的问题,本身不能说promise是异步的
.then 与 .catch
.then 可以接收两个参数,第一个是处理成功的函数,第二个是处理错误的函数。.catch 是 .then 第二个参数的简便写法,但是它们用法上有一点需要注意:.then 的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的 .catch 可以捕获之前的错误。
测试题一:
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
运行结果:1 2 4 3
解释:Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的。
测试题二:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
运行结果:
解释:promise 有 3 种状态:pending、fulfilled 或 rejected。状态改变只能是 pending->fulfilled 或者 pending->rejected,状态一旦改变则不能再变。上面 promise2 并不是 promise1,而是返回的一个新的 Promise 实例。
测试题三:
Promise.resolve()
.then(() => {
return new Error('error!!!')
})
.then((res) => {
console.log('then: ', res)
})
.catch((err) => {
console.log('catch: ', err)
})
运行结果:
then: Error: error!!!
at Promise.resolve.then (...)
at ...
解释:.then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获,需要改成其中一种:
return Promise.reject(new Error('error!!!'))
throw new Error('error!!!')
因为返回任意一个非 promise 的值都会被包裹成 promise 对象,即 return new Error('error!!!') 等价于 return Promise.resolve(new Error('error!!!'))。
测试题四:
const promise = Promise.resolve()
.then(() => {
return promise
})
promise.catch(console.error)
运行结果:
解释:.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
测试题五:
Promise.resolve('foo')
.then(Promise.resolve('bar'))
.then(function(result){
console.log(result)
})
运行结果:foo
解释:.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。then方法提供一个供自定义的回调函数,若传入非函数,则会忽略当前then方法。 回调函数中会把上一个then中返回的值当做参数值供当前then方法调用。
8、深浅拷贝
浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。
- 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
- JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”,concat、slice、Object.assign()、... 展开运算符;
- JSON.stringify 实现的是深拷贝,但是对目标对象有要求,undefined、function、symbol 会在转换过程中被忽略。。。;
- 若想真正意义上的深拷贝,请递归。
9、事件机制/Event Loop
1)、关于 JavaScript
javascript 是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。为避免阻塞又把任务分为异步和同步任务,同步任务之间进入主线程执行,异步任务进入Event Table 并注册函数。
2)、事件循环
图文结合说明:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 循环如上,也就是常说的Event Loop(事件循环)。
3)、宏任务(MacroTask)、微任务(MicroTask)
JavaScript 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask)和微任务(MicroTask)。
1.MacroTask
所有的同步任务代码都是宏任务 (MacroTask)。包括整体代码script、setTimeout、setInterval、I/O、UI Rendering 等都是宏任务。
2.MicroTask
Process.nextTick、Promise.then catch finally(注意我不是说 Promise,Promise是一个同步任务)、MutationObserver。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。