JS 之作用域 & 执行上下文 & this & 闭包

418 阅读12分钟

作用域

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

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

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

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar(); // 1

执行上下文

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

类型

  1. 全局执行上下文
    • 创建一个全局的 window 对象(浏览器的情况下)
    • 设置 this 的值等于这个全局对象
  2. 函数执行上下文
    • 每个函数都有它自己的执行上下文,不过是在函数被调用时创建的
  3. Eval函数执行上下文
    • 执行在 eval 函数内部的代码也会有它属于自己的执行上下文

执行栈(调用栈 LIFO)

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

当 JavaScript 引擎第一次遇到脚本 -----> 创建一个全局的执行上下文并且压入当前执行栈 ----->当引擎遇到一个函数调用 -----> 它会为该函数创建一个新的执行上下文并压入栈的顶部 -----> 该函数执行结束时 -----> 执行上下文从栈中弹出 -----> 控制流程到达当前栈中的下一个上下文

创建执行上下文

词法环境两种类型

  1. 全局环境
  2. 在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

1. 创建阶段

  1. this 绑定
    • 全局:this 指向全局对象(浏览器中 this 引用 window 对象)
    • 函数:this 的值取决于该函数是如何被调用的
      • 如果它被一个引用对象调用,那么 this 会被设置成那个对象
      • 否则,this 的值被设置为全局对象或者 undefined(在严格模式下)
  2. 创建词法环境组件
    • 环境记录器
      • 环境记录器是存储变量和函数声明的实际位置
      • 两种类型
        • 声明式环境记录器存储变量、函数和参数
        • 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。
        • 在全局环境中,环境记录器是对象环境记录器。在函数环境中,环境记录器是声明式环境记录器。
    • 一个外部环境的引用
      • 外部环境的引用意味着它可以访问其父级词法环境(作用域)
  3. 创建变量环境组件
    • 变量环境同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
    • 在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。

2. 执行阶段

在此阶段,完成对所有这些变量的分配,最后执行代码。

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

  • 变量对象(Variable object,VO)
    • 存储了在上下文中定义的变量和函数声明
      • 全局上下文下的变量对象
        • 全局对象
      • 函数上下文下的变量对象
        • 分析 AO
        • 执行
  • 作用域链(Scope chain)
  • this

变量对象

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

全局上下文下的变量对象

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。

在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

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

函数上下文下的变量对象

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

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

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

执行过程

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

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

变量对象会包括:

  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
}
代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值 还是上面的例子,当代码执行完后,这时候的 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. 在代码执行阶段,会再次修改变量对象的属性值

作用域链

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

函数有一个内部属性 [[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]]);

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

this

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

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

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

结论

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

从结论中我们可以看出,想要准确确定this指向,找到函数的调用者以及区分他是否是独立调用十分关键。

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

参考文章:this demo

new 过程

通过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);

参考文章:new 实现

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

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
}

call & apply 参考文章

Polyfill

es5-shim 源码模拟实现 bind

总结

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

简单理解

类的实例在调用其方法的时候,将作为主语,其方法中的this就自然变成了指代主语的代词。

class People {
  constructor(name) {
    // 在用new关键字实例化一个对象的时候,相当于在说,
    // “创建一个People类实例(主语),它(this)的name是……”
    // 所以这里的this就是新创建的People类实例
    this.name = name;
  }
  
  run() {
    console.log(`${this.name} seems happy.`)  
  }
}

// new关键字实例化一个类
var xiaoming = new People('xiaoming');
xiaoming.run(); // xiaoming seems happy.

句子的主语是可以变的,例如在下面的场景中,run被赋值到小芳(xiaofang)身上之后,调用xiaofang.run,主语就变成了小芳!

var xiaofang = {
  name: 'Xiao Fang',
};

var xiaoming = {
  name: 'Xiao Ming',
  run: function() {
    console.log(`${this.name} seems happy.`);
  },
};

xiaofang.run = xiaoming.run;
// 主语变成了小芳
xiaofang.run(); // Xiao Fang seems happy.

Function.prototype.applyFunction.prototype.call的功能是一模一样的,区别进在于,apply里将函数调用所需的所有参数放到一个数组当中。

var xiaoming = {
    name: 'Xiao Ming'
};

function run(today, mood) {
    console.log(`Today is ${today}, ${this.name} seems ${mood}`);
}

// 函数的call方法第一个参数是this的值
// 后续只需按函数参数的顺序传参即可
run.call(xiaoming, 'Monday', 'happy')

当方法失去主语的时候,this不再有?

当一个function被调用的时候是有主语的时候,它是一个方法;当一个function被调用的时候是没有主语的时候,它是一个函数。当一个函数运行的时候,它虽然没有主语,但是它的this的值会是全局对象。在浏览器里,那就是window。当然了,前提是函数没有被bind过,也不是被applycall所调用。

function作为函数的情景有哪些呢?

    1. 全局函数的调用
function bar() {
  console.log(this === window); // 输出:true
}
bar();
    1. IIFE
(function() {
  console.log(this === window); // 输出:true
})();

当函数被执行在严格模式(strict-mode)下的时候,函数的调用时的this就是undefined了。这是很值得注意的一点。

function bar() {
  'use strict';
  console.log('Case 2 ' + String(this === undefined)); // 输出:undefined
}
bar();

不可见的调用

有时候,你没有办法看到你定义的函数是怎么被调用的。因此,你就没有办法知道它的主语。下面是一个用jQuery添加事件监听器的例子。

window.val = 'window val';

var obj = {
  val: 'obj val',
  foo: function() {
    $('#text').bind('click', function() {
      console.log(this.val);
    });
  }
};

obj.foo();

本质

this 本质

demo

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
  1. checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
    globalContext.VO
];
  1. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
    checkscopeContext,
    globalContext
];
  1. checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
    Scope: checkscope.[[scope]],
}
  1. 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}
  1. 第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
  1. 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}
  1. 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
    globalContext
];

闭包

什么是闭包

闭包就是同时含有对函数对象以及作用域对象引用的最想。实际上,所有JavaScript对象都是闭包。

闭包何时被创建

因为所有JavaScript对象都是闭包,因此,当你定义一个函数的时候,你就定义了一个闭包。

闭包什么时候销毁

当它不被任何其他的对象引用的时候。

用途

定义私有变量

function Product() {
    var name;
    this.setName = function(value) {
        name = value;
    };      
    this.getName = function() {
        return name;
    }; 
}
var p = new Product(); 
p.setName("Fundebug");  
console.log(p.name); // 输出undefined 
console.log(p.getName()); // 输出Fundebug 

prototype

每个JavaScript构造函数都有一个prototype属性,用于设置所有实例对象需要共享的属性和方法。prototype属性不能列举。JavaScript仅支持通过prototype属性进行继承属性和方法。

function Rectangle(x, y) {
    this._length = x;
    this._breadth = y;
}

Rectangle.prototype.getDimensions = function() {
    return {
        length: this._length,
        breadth: this._breadth
    };
};

var x = new Rectangle(3, 4);
var y = new Rectangle(4, 3);

console.log(x.getDimensions()); // { length: 3, breadth: 4 }
console.log(y.getDimensions()); // { length: 4, breadth: 3 }

模块化

JavaScript并非模块化编程语言,至少ES6落地之前都不是。然而对于一个复杂的Web应用,模块化编程是一个最基本的要求。这时,可以使用立即执行函数来实现模块化,正如很多JS库比如jQuery以及我们Fundebug都是这样实现的。

var module = (function() {
    var N = 5;

    function print(x) {
        console.log("The result is: " + x);
    }
    
    function add(a) {
        var x = a + N;
        print(x);
    }
    
    return {
        description: "This is description",
        add: add
    };
})();

console.log(module.description); // 输出"this is description"

module.add(5); // 输出“The result is: 10”

所谓模块化,就是根据需要控制模块内属性与方法的可访问性,即私有或者公开。在代码中,module为一个独立的模块,N为其私有属性,print为其私有方法,decription为其公有属性,add为其共有方法。

变量提升

JavaScript会将所有变量和函数声明移动到它的作用域的最前面,这就是所谓的变量提升(Hoisting) 。也就是说,无论你在什么地方声明变量和函数,解释器都会将它们移动到作用域的最前面。因此我们可以先使用变量和函数,而后声明它们。

但是,仅仅是变量声明被提升了,而变量赋值不会被提升。如果你不明白这一点,有时则会出错:

console.log(y);  // 输出undefined  
y = 2; // 初始化y

上面的代码等价于下面的代码:

var y;  // 声明y

console.log(y);  // 输出undefined

y = 2; // 初始化y

为了避免BUG,开发者应该在每个作用域开始时声明变量和函数。

柯里化

柯里化,即Currying,可以是函数变得更加灵活。我们可以一次性传入多个参数调用它;也可以只传入一部分参数来调用它,让它返回一个函数去处理剩下的参数。

var add = function(x) {
    return function(y) {
        return x + y;
    }; 
};
console.log(add(1)(1)); // 输出2
var add1 = add(1); 
console.log(add1(1)); // 输出2  
var add10 = add(10); 
console.log(add10(1)); // 输出11 

apply, call与bind方法

JavaScript开发者有必要理解applycallbind方法的不同点。它们的共同点是第一个参数都是this,即函数运行时依赖的上下文。

三者之中,call方法是最简单的,它等价于指定this值调用函数:

var user = {
    name: "Rahul Mhatre",
    whatIsYourName: function() {
        console.log(this.name);
    }
};

user.whatIsYourName(); // 输出"Rahul Mhatre",

var user2 = {
    name: "Neha Sampat"
};

user.whatIsYourName.call(user2); // 输出"Neha Sampat"

apply方法与call方法类似。两者唯一的不同点在于,apply方法使用数组指定参数,而call方法每个参数单独需要指定:

  • apply(thisArg, [argsArray])
  • call(thisArg, arg1, arg2, …)

Memoization

Memoization用于优化比较耗时的计算,通过将计算结果缓存到内存中,这样对于同样的输入值,下次只需要中内存中读取结果。

function memoizeFunction(func) {
    var cache = {};     
    return function() {         
        var key = arguments[0];         
        if (cache[key]) {             
            return cache[key];         
        } else {             
            var val = func.apply(this, arguments);             
            cache[key] = val;             
            return val;         
        }     
    }; 
}  
var fibonacci = memoizeFunction(function(n) {     
    return n === 0 || n === 1 ? n : fibonacci(n - 1) + fibonacci(n - 2); 
});  
console.log(fibonacci(100)); // 输出354224848179262000000 console.log(fibonacci(100)); // 输出354224848179262000000

函数重载

所谓函数重载(method overloading) ,就是函数名称一样,但是输入输出不一样。或者说,允许某个函数有各种不同输入,根据不同的输入,返回不同的结果。凭直觉,函数重载可以通过if…else或者switch实现,这就不去管它了。jQuery之父John Resig提出了一个非常巧(bian)妙(tai)的方法,利用了闭包。

从效果上来说,people对象的find方法允许3种不同的输入: 0个参数时,返回所有人名;1个参数时,根据firstName查找人名并返回;2个参数时,根据完整的名称查找人名并返回。

难点在于,people.find只能绑定一个函数,那它为何可以处理3种不同的输入呢?它不可能同时绑定3个函数find0,find1find2啊!这里的关键在于old属性。

addMethod函数的调用顺序可知,people.find最终绑定的是find2函数。然而,在绑定find2时,oldfind1;同理,绑定find1时,oldfind0。3个函数find0,find1find2就这样通过闭包链接起来了。

根据addMethod的逻辑,当f.lengtharguments.length不匹配时,就会去调用old,直到匹配为止。

function addMethod(object, name, f) {
    var old = object[name];
    object[name] = function() {
        // f.length为函数定义时的参数个数
        // arguments.length为函数调用时的参数个数
        if (f.length === arguments.length) {
            return f.apply(this, arguments);
        } else if (typeof old === "function") {
            return old.apply(this, arguments);
        }
    };
}

// 不传参数时,返回所有name
function find0() {
    return this.names;
}

// 传一个参数时,返回firstName匹配的name
function find1(firstName) {
    var result = [];
    for (var i = 0; i < this.names.length; i++) {
        if (this.names[i].indexOf(firstName) === 0) {
            result.push(this.names[i]);
        }
    }
    return result;
}

// 传两个参数时,返回firstName和lastName都匹配的name
function find2(firstName, lastName) {
    var result = [];
    for (var i = 0; i < this.names.length; i++) {
        if (this.names[i] === firstName + " " + lastName) {
            result.push(this.names[i]);
        }
    }
    return result;
}

var people = {
    names: ["Dean Edwards", "Alex Russell", "Dean Tom"]
};

addMethod(people, "find", find0);
addMethod(people, "find", find1);
addMethod(people, "find", find2);

console.log(people.find()); // 输出["Dean Edwards", "Alex Russell", "Dean Tom"]
console.log(people.find("Dean")); // 输出["Dean Edwards", "Dean Tom"]
console.log(people.find("Dean", "Edwards")); // 输出["Dean Edwards"]

demo

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}

console.log(new Date, i);

打印:第 1 个 5 直接输出,1 秒之后,输出 5 个 5

2021-09-27T09:45:10.246Z 5
2021-09-27T09:45:11.252Z 5
2021-09-27T09:45:11.252Z 5
2021-09-27T09:45:11.253Z 5
2021-09-27T09:45:11.253Z 5
2021-09-27T09:45:11.253Z 5

输出:5 -> 0,1,2,3,4

期望代码的输出变成:5 -> 0,1,2,3,4

闭包

IIFE

for (var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000)
    })(i)
}

console.log(new Date, i)

API

for (var i = 0; i < 5; i++) {
    setTimeout(function(j) {
        console.log(new Date, j);
    }, 1000, i)
}

console.log(new Date, i)

Function

function output(i) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000)
}
for (var i = 0; i < 5; i++) {
    output(i)
}

console.log(new Date, i)

输出:0 -> 1 -> 2 -> 3 -> 4 -> 5

期望代码的输出变成 0 -> 1 -> 2 -> 3 -> 4 -> 5,并且要求原有的代码块中的循环和两处 console.log 不变,该怎么改造代码

代码执行时,立即输出 0,之后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5

IIFE + setTimeout

for (var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000 * i);
    })(i)
}

setTimeout(() => {
    console.log(new Date, i);
}, 1000 * i)

output:

2021-09-28T01:58:47.234Z 0
2021-09-28T01:58:48.235Z 1
2021-09-28T01:58:49.236Z 2
2021-09-28T01:58:50.238Z 3
2021-09-28T01:58:51.236Z 4
2021-09-28T01:58:52.239Z 5

ES6 Promise

const tasks = []
for (var i =0; i < 5; i++) {
    ((j) => {
        tasks.push(new Promise(resolve => {
            setTimeout(() => {
                console.log(new Date, j)
                resolve()
            }, 1000 * j)
        }))
    })(i)
}

Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i)
    }, 1000)
})

output:

2021-09-28T02:07:26.537Z 0
2021-09-28T02:07:27.536Z 1
2021-09-28T02:07:28.537Z 2
2021-09-28T02:07:29.537Z 3
2021-09-28T02:07:30.536Z 4
2021-09-28T02:07:31.540Z 5

ES6 Promise 代码优化

模块化

const tasks = []

const output = (i) => new Promise(resolve => {
    setTimeout(() => {
        console.log(new Date, i)
        resolve()
    }, 1000 * i)
})
for (var i =0; i < 5; i++) {
    tasks.push(output(i))
}

Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i)
    }, 1000)
})
const tasks = []
const defaultCallback = () => {}

const timer = (timeout, useCallback = defaultCallback) =>{
    setTimeout(useCallback, timeout);
}

const output = (i) => new Promise(resolve => {
    timer(1000 * i, () => {
        console.log(new Date, i)
        resolve()
    })
})
for (var i =0; i < 5; i++) {
    tasks.push(output(i))
}

Promise.all(tasks).then(() => {
    timer(1000, () => {
        console.log(new Date, i)
    })
})

ES7 async/await

const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS)
});

(async () => {  // 声明即执行的 async 函数表达式
    for (var i = 0; i < 5; i++) {
        if (i > 0) {
            await sleep(1000)
        }
        console.log(new Date, i)
    }

    await sleep(1000)
    console.log(new Date, i)
})()

参考文章

执行上下文

变量对象

作用域链

this 本质

ECMAScript 5.1 规范

闭包

demo