JS

241 阅读9分钟

1. 执行上下文

JS代码运行的时候都在执行上下文中运行。

类型

  1. 全局执行上下文

    • 创建一个全局的 window 对象(浏览器的情况下)
    • 设置 this 的值等于这个全局对象
  2. 函数执行上下文

    • 每个函数都有它自己的执行上下文,不过是在函数被调用时创建的
  3. Eval函数执行上下文

    • 执行在 eval 函数内部的代码也会有它属于自己的执行上下文

执行栈(调用栈 LIFO)

用来存储代码运行时创建的所有执行上下文。

当 JavaScript 引擎第一次遇到脚本 ----->

创建一个全局的执行上下文并且压入当前执行栈 ----->

当引擎遇到一个函数调用 -----> 它会为该函数创建一个新的执行上下文并压入栈的顶部 ----->

该函数执行结束时 -----> 执行上下文从栈中弹出 ----->

控制流程到达当前栈中的下一个上下文

每个执行上下文有3个重要属性

  • 变量对象(Variable object,VO)

    • 存储了在上下文中定义的变量和函数声明

      • 全局上下文下的变量对象

        • 全局对象
      • 函数上下文下的变量对象

        • 分析 AO
        • 执行
  • 作用域链(Scope chain)

  • this

变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

全局上下文中的变量对象就是全局对象!!!

函数上下文下的变量对象

定义(此处可以忽略)

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象何时创建(活动对象其实就是变量对象)

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

执行过程

执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:

  1. 进入执行上下文
  2. 代码执行
1. 进入执行上下文

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明

    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};b = 3;
​
}
​
foo(1);

在进入执行上下文后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}
2. 代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值 还是上面的例子,当代码执行完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

总结:变量对象的创建过程

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  4. 在代码执行阶段,会再次修改变量对象的属性值

2. 作用域

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

3. 作用域链

函数的作用域在函数定义的时候就决定了。

函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。

function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO
];
​
bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。

这时候执行上下文的作用域链,我们命名为 Scope:

Scope = [AO].concat([[Scope]]);

至此,作用域链创建完毕。

4. 闭包

定义

闭包是指那些能够访问自由变量的函数。

自由变量:在函数中使用的既不是函数参数也不是函数局部变量的变量。

  1. 理论上来讲:所有的函数都有可能是闭包,函数中去访问全局变量相当于是在访问自由变量。
  2. 实践角度: 2.1 即使创建它的上下文已经被销毁了,它仍然存在(内部函数从父函数中返回) 2.2 代码中引用了自由变量

应用场景

4.1 柯里化函数

避免频繁调用具有相同参数的函数,同时能够轻松的复用。 其实就是封装一个高阶函数(返回值是函数的函数)。

function getArea(width, height) { 
 return width + height
 } 
​
const area1 = getArea(10, 20) 
const area2 = getArea(10, 30) 
const area3 = getArea(10, 40) 
const area4 = getArea(10, 50) 
​
​
function getArea(width) { 
 return height => { 
 return width + height
 } 
 } 
const getAreaWidth = getArea(10) 
const area1 = getAreaWidth(20) 
const area2 = getAreaWidth(30) 
const area3 = getAreaWidth(40) 
const area4 = getAreaWidth(50) 

4.2 使用闭包实现私有方法/变量

模块化:现代化的打包方式,最终就是每个模块的代码都是相互独立的。

function funOne(i) { 
 function funTwo() { 
 console.log('数字', i) 
 } 
 return funTwo
 } 
​
const fa = funOne(110) // 110
const fb = funOne(111) // 111
const fc = funOne(112) // 112

4.3 匿名自执行函数

var funOne = (function() { 
 var num = 0
 return function() { 
 num ++ 
 return num
 } 
 })() 
​
console.log(funOne()) // 1
console.log(funOne()) // 2
console.log(funOne()) // 3

4.4 缓存一些结果

外部函数中创建一个数组,闭包函数可以获取或者修改这个数组的值,延长了变量的生命周期。

function parent() { 
 let arr = [] 
 function child(i) { 
 arr.push(i) 
 console.log(arr.join()) 
 } 
 return child
 } 
​
const fn = parent() 
fn(1) // 1
fn(2) // 1,2

好处

  1. 避免污染全局变量

总结

  1. 创建私有变量;

  2. 延长变量的生命周期;

  3. 缺点:

    1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
    2. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

面试高频题

**1. 实现一个 compose 函数

function fn1(x) {
  return x + 1
}

function fn2(x) {
  return x + 2
}

function fn3(x) {
  return x + 3
}

function fn4(x) {
  return x + 4
}

const a = compose(fn1, fn2, fn3, fn4)
console.log(a(1)) // 1 + 4 + 3 + 2 + 1 = 11

// 实现
function compose() {
}

实现

// 实现
function compose() {
  const argsFnList = [...arguments]
  return (arg) => argsFnList.reduce((pre, cur) => cur(pre), arg)
}

2. 柯里化

function currying(fn, ...args) {
}
const add = (a, b, c) => a + b + c
const a1 = currying(add, 1)
const a2 = a1(2)
console.log(a2(3)) // 1 + 2 + 3 = 6

实现

function currying(fn, ...args) {
  const originLength = fn.length
  let allArgs = [...args]

  const resFn = (...newArgs) => {
    allArgs = [...allArgs, ...newArgs]
    
    if (allArgs.length === originLength) {
      return fn(...allArgs)
    } else {
      return resFn
    }
  }
  return resFn
}

3. 实现 compose

let middlewares = []

middlewares.push((next) => {
  console.log(1)
  next()
  console.log(6)
})

middlewares.push((next) => {
  console.log(2)
  next()
  console.log(5)
})

middlewares.push((next) => {
    console.log(3)
    next()
    console.log(4)
})


let fn = compose(middlewares)
fn() // 输出 1 2 3 4 5 6 洋葱模型

// 实现
function compose() {
}

实现

function compose(middlewares) {
  const copyMiddlewares = [...middlewares]
  let index = 0
  
  const fn = () => {
    if (index >= middlewares.length) {
      return
    }
  
    const currentMiddleware = copyMiddlewares[index++]
    return currentMiddleware(fn)
  }
  
  return fn
}

4. koa-compose

const compose = require('koa-compose');

function one(ctx,next){
    console.log('第一个');
    next(); // 控制权交到下一个中间件(实际上是可以执行下一个函数),
}
function two(ctx,next){
    console.log('第二个');
    next();
}
function three(ctx,next){
    console.log('第三个');
    next();
}
// 传入中间件函数组成的数组队列,合并成一个中间件函数
const middlewares = compose([one, two, three]);
// 执行中间件函数,函数执行后返回的是Promise对象
middlewares().then(function (){
    console.log('队列执行完毕');    
})

实现

function compose (middleware) {
    // 如果传入的不是数组,则抛出错误
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    // 数组队列中有一项不为函数,则抛出错误
    for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }

    // compose函数调用后,返回的是以下这个匿名函数
    // 匿名函数接收两个参数,第一个根据使用场景决定
    // 第一次调用时候第二个参数next实际上是一个undefined,因为初次调用并不需要传入next参数
    // 这个匿名函数返回一个 promise
    return function(context, next) {
        //初始下标为-1
        let index = -1
        return dispatch(0)
        function dispatch (i) {
            // 如果传入i为负数且<=-1 返回一个Promise.reject携带着错误信息
            // 所以执行两次next会报出这个错误。将状态rejected,就是确保在一个中间件中next只调用一次
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))

            // 执行一遍next之后,这个index值将改变
            index = i

            // 根据下标取出一个中间件函数
            let fn = middleware[i]

            // next在这个内部中是一个局部变量,值为undefined
            // 当i已经是数组的length了,说明中间件函数都执行结束,执行结束后把fn设置为undefined
            // 问题:本来middleware[i]如果i为length的话取到的值已经是undefined了,为什么要重新给fn设置为undefined呢?
            if (i === middleware.length) fn = next
      
            //如果中间件遍历到最后了。那么。此时return Promise.resolve()返回一个成功状态的promise
            // 方便之后做调用then
            if (!fn) return Promise.resolve()
      
            // try catch保证错误在Promise的情况下能够正常被捕获。
            // 调用后依然返回一个成功的状态的Promise对象
            // 用Promise包裹中间件,方便await调用
            // 调用中间件函数,传入context(根据场景不同可以传入不同的值,在KOa传入的是ctx)
            // 第二个参数是一个next函数,可在中间件函数中调用这个函数
            // 调用next函数后,递归调用dispatch函数,目的是执行下一个中间件函数
            // next函数在中间件函数调用后返回的是一个promise对象
            // 读到这里不得不佩服作者的高明之处。
            try {
                return Promise.resolve(fn(context, function next () {
                    return dispatch(i + 1)
                }))
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}

5. this

如果要判断一个函数的this绑定,就需要找到这个函数的直接调用位置。然后可以顺序按照下面四条规则来判断this的绑定对象:

  1. new调用:绑定到新创建的对象
  2. callapplybind调用:绑定到指定的对象
  3. 由上下文对象调用:绑定到上下文对象
  4. 默认:全局对象

注意:箭头函数不使用上面的绑定规则,根据外层作用域来决定this,继承外层函数调用的this绑定。

结论

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

demo 分析

var a = 20;
var obj = {
  a: 10,
  c: this.a + 20,
  fn: function () {
    return this.a;
  }
}

console.log(obj.c); // 40
console.log(obj.fn()); // 10

单独的{}不会形成新的作用域,因此这里的this.a,由于并没有作用域的限制,它仍然处于全局作用域之中。所以这里的this其实是指向的window对象。

var a = 20;
var foo = {
  a: 10,
  getA: function () {
    return this.a;
  }
}
console.log(foo.getA()); // 10

var test = foo.getA;
console.log(test());  // 20

foo.getA()中,getA是调用者,他不是独立调用,被对象foo所拥有,因此它的this指向了foo。而test()作为调用者,尽管他与foo.getA的引用相同,但是它是独立调用的,因此this指向undefined,在非严格模式,自动转向全局window。

var a = 20;
function getA() {
  return this.a;
}
var foo = {
  a: 10,
  getA: getA
}
console.log(foo.getA());  // 10
function foo() {
  console.log(this.a)
}

function active(fn) {
  fn(); // 真实调用者,为独立调用
}

var a = 20;
var obj = {
  a: 10,
  getA: foo
}

active(obj.getA); // 20

6. new 过程

4个阶段

通过new操作符调用构造函数,会经历以下4个阶段:

  • 创建一个新的对象;
  • 将构造函数的this指向这个新对象;
  • 指向构造函数的代码,为这个对象添加属性,方法等;
  • 返回新对象。

详细解析

  • 创建了一个全新的对象,这个对象会被执行[[Prototype]](也就是__proto__)链接。
  • 生成的新对象会绑定到函数调用的this
  • 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
  • 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

实现 new 过程

/**
 * 模拟实现 new 操作符
 * @param  {Function} ctor [构造函数]
 * @return {Object|Function|Regex|Date|Error}      [返回结果]
 */
const newOperator = (function() { 
    // 使用栈存储的目的:确保多次 new 时 target 取到对应的值
    const _newStack = []; 
    function newOperator(ctor) { 
        // 设定new.target 
        newOperator.target = ctor; 
        // 生成新的对象,其隐式原型指向构造函数的原型对象 
        const obj = Object.create(ctor.prototype); 
        // 执行构造函数,并返回结果 
        const result = ctor.apply(obj, Array.prototype.slice.call(arguments, 1)); 
        // 重置new.target 
        newOperator.target = null; 
        // 判断最终返回对象 
        return ((typeof result === 'object' && result !== null) || typeof result === 'function') ? result : obj; 
    } 
    // 设定target的get、set方法 
    Reflect.defineProperty(newOperator, 'target', {
        get() {
            return _newStack[_newStack.length - 1];
        },
        set(target) {
            if (target == null) { 
                _newStack.pop(); 
            } else { 
                _newStack.push(target); 
            } 
        } 
    }) 
    return newOperator; 
})(); 
function B() { 
    if (newOperator.target === B) { 
        console.log('new调用 B') 
    } else {
        console.log('非new调用 B') 
    } 
    return {balabala: 123}; 
} 
function A() { 
    const b = newOperator(B); 
    if (newOperator.target === A) { 
        console.log('new调用 A') 
    } else { 
        console.log('非new调用 A') 
    } 
} 
newOperator(A);

对于不支持ES5的浏览器,MDN上提供了polyfill方案

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;
        return new F();
    };
}

实现 call、apply、bind

call

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数
Function.prototype.call2 = function(context) { 
    // 首先要获取调用call的函数,用this可以获取 
    context.fn = this;
    context.fn(); 
    delete context.fn; 
}

问题一:从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里

Function.prototype.call2 = function(context) { 
    context.fn = this; 
    var args = []; 
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']'); 
    }
    eval('context.fn(' + args +')'); 
    delete context.fn; 
}

问题二:

1.this 参数可以传 null,当为 null 的时候,视为指向 window

2.函数是可以有返回值的!

Function.prototype.call2 = function (context) { 
    var context = context || window; 
    context.fn = this; 
    var args = []; 
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']'); 
    } 
    var result = eval('context.fn(' + args +')'); 
    delete context.fn 
    return result; 
}

apply

apply 的实现跟 call 类似

Function.prototype.apply = function (context, arr) {
    var context = Object(context) || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

bind

MDN polyfill

Function.prototype.bind = function (context) {
    var context = context || window
    var oThis = this

    var args = Array.prototype.slice.call(arguments, 1)

    var fNop = function(){}
    var fBound = function () {
        return oThis.apply(this instanceof fNop ? this : context || this,
            args.concat(Array.prototype.slice.call(arguments)))
    }

    fNop.prototype = oThis.prototype
    fBound.prototype = new fNop()

    return fBound
}

总结

  • 三者都是用来改变函数的this指向
  • 三者的第一个参数都是this指向的对象
  • bind是返回一个绑定函数可稍后执行,callapply是立即调用
  • 三者都可以给定参数传递
  • call给定参数需要将参数全部列出,apply给定参数数组

7. 原型

JavaScript 中万物皆对象,对象皆出自构造函数。

对象分为:函数对象和普通对象。

对象独有:__proto__ constructor

函数独有:prototype

JavaScript 中函数也是对象,故函数也拥有__proto__ constructor 属性。

var A = function(){}; // A是一个方法,当然也是个对象  
var a = new A();

image.png

定义

给其它对象提供共享属性的对象。

prototype 自己也是对象,只是被用以承担了某个职能。

公式

实例.__proto__ === 实例的构造函数.prototype

JavaScript 中的 `Object``Function` 就是典型的函数对象。  

所有函数对象的__proto__都指向Function.prototype

8. 原型链

原型链的形成是靠 __proto__ 而非prototype

instanceof

object instanceof constructor

instanceof 用来检测 contructor.prototype 是否在 object 的原型链上。

function instance_of(l, r) {
    var rProto = r.prototype
    var lProto = l.__proto__
    console.log(typeof rProto)
    console.log(typeof lProto)
    if (!lProto) return false
    while(l) {
        console.log(typeof lProto)
        if (lProto === rProto) {
            return true
        }
        lProto = lProto.__proto__
    }
}

解析

Object instanceof Object

rProto = Object.prototype
lProto = Object.__proto__ = Function.prototype// 进入循环体,第一次循环
lProto !== rProto
lProto = lProto.__proto__ = Function.prototype.__proto__ = Object.prototype// 第二次循环
lProto === rProto

Function instanceof Function

9. 继承

9.1 ES5 中的继承实现方式

9.1.1 new 关键字

  1. 创建对象 obj;
  2. 将对象 obj 的原型指向构造函数,使对象 obj 可以访问到构造函数原型对象的属性;
  3. 将构造函数的 this 绑定到当前对象 obj,使对象 obj 可以访问到构造函数的属性;
  4. 如果构造函数返回一个对象,那么我们返回这个对象,否则返回对象 obj。

以 new 操作符调用构造函数的时候,函数内部实际上发生以下变化:

  1. 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
  2. 属性和方法被加入到 this 引用的对象中。
  3. 新创建的对象由 this 所引用,并且最后隐式的返回 this.
function objectFactory() {  
 var obj = new Object()  
 
 var Constructor = [].shift.call(arguments) // 第一个参数就是构造函数
 
 function F(){}  
 F.prototype = Constructor.prototype
 obj = new F()  
 
 var result = Constructor.apply(obj, arguments)  
 return typeof result === 'object' ? result : obj
  }  

9.1.2 类式继承

子元素.prototype.__proto__ = 父元素.prototype
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;  
  }  
​
var instance = new SubClass();  
​
console.log(instance instanceof SuperClass) // true
console.log(instance instanceof SubClass) // true
console.log(SubClass instanceof SuperClass)// false

缺点

  1. 子类通过 prototype 对父类实例化,继承了父类的属性和方法,如果父类中有引用类型的属性,会被共享。一个子类实例对共享属性的修改会对其他实例的属性造成影响。
  2. 子类的继承是靠其原型prototype属性对父类实例进行实例化的,因此不能对父类传递参数。

9.1.3 构造函数继承

function SuperClass() {  
 this.superValue = true;  
  }  
SuperClass.prototype.getSuperValue = function() {  
 return this.superValue;  
  }  
​
function SubClass() {  
 SuperClass.call(this, arguments)  
  }  

缺点

  1. 父类的原型方法不会被继承;
  2. 如果想被继承,就需要把 prototype 放到构造函数中,这样创建的每个实例都会有一套单独的属性和方法,不能复用。

9.1.4 组合继承

function SuperClass() {  
 this.superValue = true;  
  }  
SuperClass.prototype.getSuperValue = function() {  
 return this.superValue;  
  }  
​
function SubClass() {  
 SuperClass.call(this, arguments)  
  }  
​
SubClass.prototype = new SuperClass()  

缺点

父类构造函数被调用2次。

9.1.5 原型式继承

function inheritObject(o) {  
  //声明一个过渡对象
 function F() { }  
  //过渡对象的原型继承父对象
 F.prototype = o;  
  //返回过渡对象的实例,该对象的原型继承了父对象
 return new F();  
  }  

缺点

  1. 原型式继承和类式继承一个样子,对于引用类型的变量,还是存在子类实例共享的情况。

9.1.6 寄生式继承

function CreateObj(obj) {  
 var o = inheritObject(obj)  
 o.method = function(){}  
 return o
  }  

9.1.7 寄生组合式继承

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;  
  }  

ES6 中的继承-Class继承

class 中继承主要是依靠两个东西:

  • extends

  • super

    而且对于该继承的效果和寄生组合继承方式一样。

    ES6 继承

    10. 深浅拷贝

    深拷贝和浅拷贝都是针对的引用类型。

    10.1 浅拷贝

    浅拷贝就是只复制引用,而未复制真正的值。

    JavaScript中的拷贝方法都是首层浅拷贝

    concat、slice、Object.assign()、 ...展开符 都是首层浅拷贝。

    10.2 深拷贝

    目前实现深拷贝的方法主要是两种:

    利用 JSON 对象中的 parse 和 stringify; 利用递归来实现每一层都重新创建对象并赋值。

    10.2.1 JSON.parse(JSON.stringify(obj))

    如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝,因为 undefined、function、symbol 会在转换过程中被忽略。

    10.2.2 递归

    对每一层的数据都实现一次 创建对象->对象赋值 的操作。

    function isObject(obj) {
        return obj && typeof obj === 'object'
    }
    function deepClone(obj) {
        if (!isObject(obj)) return obj
        let targetObj = obj.constructor === Array ? [] : {}
        for (let key in obj) {
            if (Object.prototype.hasOwnProperty.call(obj, key)) {
                if (isObject(obj[key]) {
                    targetObj[key] = deepClone(obj[key])
                } else {
                    targetObj[key] = obj[key]
                }
            }
        }
    }
    

    总结

    1. 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
    2. JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
    3. JSON.stringify 实现的是深拷贝,但是对目标对象有要求:非function, undefined,symbol;
    4. 若想真正意义上的深拷贝,用递归实现。

    11. Event Loop

    浏览器的 Event Loop

    Node中的 Event Loop

    12. Promise

    Promise 详解