1

执行上下文

执行上下文(Execution Contexts),简称上下文,是一种规范策略,用于跟踪ECMAScript实现对于代码运行时的评估。在任何时间点,每个实际执行代码的代理最多有一个执行上下文。 这称为代理的运行执行上下文(running execution context)。

简而言之,变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。

上下文一共有以下三种:

  • 全局上下文
  • 函数上下文(局部上下文)
  • eval()调用内部的上下文

执行上下文栈

执行上下文堆栈(execution context stack)用于跟踪执行上下文。 正在运行的执行上下文始终是此堆栈的顶部元素。 每当控制从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文无关的可执行代码时,就会创建一个新的执行上下文。 新创建的执行上下文被压入堆栈,成为运行的执行上下文。

全局上下文

全局上下文是最外层的上下文。根据ECMAScript实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器环境中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用letconst的顶级声明不会定义在全局上下文中,但在作用域链解析效果上是一样的。

函数上下文

每个函数调用都有自己的函数上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完成之后,上下文栈就会弹出该函数上下文,将控制权返还给之前的执行上下文。

eval()调用内部的上下文

在非严格模式下,eval函数内部变量的声明会影响调用上下文(callerContext

"use strict";

var x = 1;
let y = 3;
eval("var x = 2;let y = 4;");
eval("console.log(x, y);"); // 严格模式输出1 3;非严格模式输出2 3
console.log(x, y); // 严格模式输出1 3;非严格模式输出2 3
如果调用上下文的代码或eval码是严格模式代码,则eval代码不能实例化调用eval的调用上下文的变量环境中的变量或函数绑定。 相反,这样的绑定在一个新的VariableEnvironment中实例化,只有eval代码可以访问。 由letconstclass声明引入的绑定总是在新的LexicalEnvironment中实例化。

What's the difference between "LexicalEnvironment" and "VariableEnvironment" in spec

A LexicalEnvironment is a local lexical scope, e.g., for let-defined variables. If you define a variable with let in a catch block, it is only visible within the catch block, and to implement that in the spec, we use a LexicalEnvironment. VariableEnvironment is the scope for things like var-defined variables. vars can be thought of as "hoisting" to the top of the function. To implement this in the spec, we give functions a new VariableEnvironment, but say that blocks inherit the enclosing VariableEnvironment.

所以,非严格模式下,使用var声明的变量会影响调用上下文,由letconstclass声明的变量不会影响调用上下文。

作用域

在代码执行之前,所有ECMAScript代码都必须与作用域(Realms)相关联。 从概念上讲,一个作用域由一组内在对象、一个ECMAScript全局环境、在该全局环境范围内加载的所有ECMAScript代码以及其他相关的状态和资源组成。

当我们创建了一个函数或者 {} 块,就会生成一个新的作用域。需要注意的是,通过 var 创建的变量只有函数作用域,而通过 letconst 创建的变量既有函数作用域,也有块作用域。

作用域分为以下两种:

  • 词法作用域(静态作用域)
  • 动态作用域

词法作用域

What is lexical scope?

词法作用域指一个函数由定义即可确定能访问的作用域,在编译时即可推导出来。
function foo() {
    let a = 5;

    function foo2() {
        console.log(a);
    }

    return foo2;
}

动态作用域

动态作用域,指函数由调用函数的作用域链确定的可访问作用域,是动态的。
function fn() {
    console.log('隐式绑定', this.a);
}
const obj = {
    a: 1,
    fn
}

obj.fn = fn;
obj.fn();

作用域链

每一个作用域都有对其父作用域的引用。当我们使用一个变量的时候,Javascript引擎 会通过变量名在当前作用域查找,若没有查找到,会一直沿着作用域链一直向上查找,直到 global 全局作用域。作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。

作用域链增强

某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。

  • try/catch语句的catch
  • with语句

    function buildUrl() {
      let qs = "?debug=true";
    
      with(location) {
          let url = href + qs;
      }
    
      return url;
    }

this指向问题

this是在执行时动态读取上下文决定的,而不是创建时

全局上下文中的this

无论是否在严格模式下,在全局执行上下文中(在任何函数体外部)this都指代全局对象

// 在浏览器中,window对象同时也是全局对象
console.log(this===window); // true

函数上下文中的this

在函数内部,this的值取决于函数被调用的方式

函数直接调用

函数直接调用中,非严格模式下this指向的是window,严格模式下this指向的是undefined

function foo() {
    console.log('函数内部this', this);
}

foo();

隐式绑定

this指向它的调用者,即谁调用函数,他就指向谁。

function fn() {
    console.log('隐式绑定', this.a);
}
const obj = {
    a: 1,
    fn
}

obj.fn = fn;
obj.fn(); // 1

显式绑定

通过callapplycall方法改变this的行为。

var name = 'a';
function foo() {
    console.log('函数内部this', this.name);
}

foo(); // a
foo.call({name: 'b'}); // b
foo.apply({name: 'c'}); // c
const bindFoo = foo.bind({name: 'd'});
bindFoo(); // d

call、apply、bind的区别:

  • call基本等价于apply,传参方式不同, call为依次传入function.call(thisArg, arg1, arg2, ...)apply是数组传入func.apply(thisArg, [argsArray])
  • callapply是直接返回改变this指向之后函数的执行结果,而bind是返回改变了this指向的函数

构造函数的new绑定

当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象。

new关键字会进行如下操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。

箭头函数中的this

箭头函数中,this与封闭词法上下文的this保持一致。

setTimeout回调函数中的this

setTimeout回调函数中的this指向window。可以使用箭头函数作为回调函数,让回调中的this指向父级作用域中的this。

闭包

MDN的解释:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。

下面介绍一些闭包的应用:

事件处理(异步执行)的闭包

解决var的变量提升问题

    let lis = document.getElementsByTagName('li');

    for(var i = 0; i < lis.length; i++) {
        (function(i) {
            lis[i].onclick = function() {
                console.log(i);
            }
        })(i);
    }

实现私有变量

function People(num) { // 构造函数
    let age = num;
    this.getAge = function() {
        return age;
    };
    this.addAge = function() {
        age++;
    };
}

let tom = new People(18);
let pony = new People(20);
console.log(tom.getAge()); // 18
pony.addAge();
console.log(pony.getAge()); // 21

装饰器函数

A function decorator is a higher-order function that takes one function as an argument and returns another function, and the returned function is a variation of the argument function—Javascript Allongé
  • 函数防抖与函数节流
  • once(fn)

    function once(fn){
      let returnValue;
      let canRun = true;
      return function runOnce(){
          if(canRun) {
              returnValue = fn.apply(this, arguments);
              canRun = false;
          }
          return returnValue;
      }
    }
    var processonce = once(process);
    processonce(); //process
    processonce(); //

使用闭包绑定函数上下文(实现bind函数的功能)

function.bind(thisArg[, arg1[, arg2[, ...]]])
arg1, arg2, ...表示当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

// bind应该挂在Function的原型下
Function.prototype.myBind = function() {
    let fn = this; // 需要使用bind改变this指向的原函数
    let args = [...arguments];
    let newThis = args.shift();
    // bind返回一个函数
    return function() {
        // bind返回的函数执行时需要返回绑定新this的原函数的执行结果
        return fn.apply(newThis, args.concat(...arguments));
    }
}

// 实现apply
Function.prototype.myApply = function() {
    let fn = this;
    let args = [...arguments];
    let newThis = args.shift();
    
    if(args.length > 0) {
        args = args[0];
    } else {
        args = [];
    }

    // 临时挂载函数
    newThis.fn = fn;

    // 执行挂载函数
    let result = newThis.fn(...args);

    // 销毁临时挂载
    delete newThis.fn;

    return result;
}

// 测试
let obj = {
    a: 1
}

function foo(b) {
    console.log(this.a, b);
}

foo.myApply(obj, [2]); // 1 2

let foo1 = foo.myBind(obj, 2);
foo1(); // 1 2

实战练习

写出如下代码的输出结果:

const foo = {
    bar: 10,
    fn: function() {
        console.log(this.bar);
        console.log(this);
    }
}
// 取出函数
let fn1 = foo.fn;
// 单独执行
fn1();

// 输出undefined  window对象

如何改变fn的指向:

const o1 = {
    text: 'o1',
    fn: function() {
        // 直接使用上下文 - 传统分活
        return this.text;
    }
}

const o2 = {
    text: 'o2',
    fn: function() {
        // 呼叫领导执行 - 部门协作
        return o1.fn();
    }
}

const o3 = {
    text: 'o3',
    fn: function() {
        // 直接内部构造 - 公共人
        let fn = o1.fn;
        return fn();
    }
}

console.log('o1fn', o1.fn()); // o1
console.log('o2fn', o2.fn()); // o1
console.log('o3fn', o3.fn()); // undefined 因为先取出了o1对象的fn,然后再直接执行

现在我要使console.log('o2fn', o2.fn())的输出结果为o2:

 const o1 = {
    text: 'o1',
    fn: function() {
        return this.text;
    }
}

const o2 = {
    text: 'o2',
    // 将o1的fn抢过来,挂到o2下面
    fn: o1.fn
}

console.log('o2fn', o2.fn());

feipeng123s
16 声望3 粉丝