执行上下文

执行上下文生命周期
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

可执行代码
这就要说到 JavaScript 的可执行代码(executable code)的类型有哪些了?
其实很简单,就三种,全局代码、函数代码、eval代码。
举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做”执行上下文(execution contexts)”。

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向,谨记this的指向,是在函数被调用的时候确定的。也就是执行上下文被创建时确定的。且在函数执行过程中,this一旦被确定,就不可更改了

this 并不是取决于他所在的位置,而是取决于他所在的Function是如何调用的
构造函数中的 this ,就是指向即将实例化的那个对象。谨记!

全局上下文

在全局执行上下文中this都指代全局对象。
在浏览器里面this等价于window对象,如果你声明一些全局变量,这些变量都会作为this的属性。

1
2
3
4
5
6
7
8
9
10
// 通过this绑定到全局对象
this.a2 = 20;
// 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身
var a1 = 10;
// 仅仅只有赋值操作,标识符会隐式绑定到全局对象
a3 = 30;
// 输出结果会全部符合预期
console.log(a1);
console.log(a2);
console.log(a3);

函数上下文,函数中的this

  • demo
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var a = 20;
    var obj = {
    a: 10,
    c: this.a + 20,
    fn: function () {
    console.log(this)
    return this.a;
    }
    }
    console.log(obj.c);
    console.log(obj.fn());
    注意这里的
    obj: {
    c: this.a + 20
    }

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

直接调用

this指向全局变量, this === window

1
2
3
4
function foo(){
return this;
}
console.log(foo() === window); // true

作为对象的一个方法

this指向调用函数的对象

1
2
3
4
5
6
7
var person = {
name: "cpp",
getName: function(){
return this.name;
}
}
console.log(person.getName()); // cpp

稍不留心写成了

1
2
3
4
5
6
7
var person = {
name: "cpp",
getName: () => {
return this.name;
}
}
console.log(person.getName()); // '' 返回空字符串

这是因为下面要说的箭头函数

箭头函数

所有的箭头函数都没有自己的this,都指向外层,
1.没有自己的this、super、arguments和new.target绑定。
2.不能使用new来调用。
3.没有原型对象。
4.不可以改变this的绑定。
5.形参名称不能重复。
MDN中的解释

An arrow function does not create its own this, the this value of the enclosing execution context is used.
箭头函数会捕获其所在上下文的this值,作为自己的this值。

1
2
3
4
5
6
7
8
9
function Person(name){
this.name = name;
this.say = () => {
var name = "xb";
return this.name;
}
}
var person = new Person("sb");
console.log(person.say()); // sb

哪些场景下不能使用箭头函数

1.在一个对象上,定义一个指向函数的属性,当方法被调用时,方法内的this指向方法所属的对象

1
2
3
4
5
6
7
8
9
10
const calculator = {
array: [1, 2, 3],
sum: () => {
console.log(this === window); // => true
return this.array.reduce((result, item) => result + item);
}
};
console.log(this === window); // => true
// Throws "TypeError: Cannot read property 'reduce' of undefined"
calculator.sum();

2.定义原型方法
3.定义事件回调函数
4.定义构造函数

作为一个构造函数

this被绑定到正在构造的实例中

1
2
3
4
5
6
7
8
9
10
function Person(name){
this.name = name;
this.age = 30;
this.say = function(){
console.log(this.name + ":" + this.age);
}
}
var person = new Person("cpp");
console.log(person.name); // cpp
person.say(); // cpp:30

this绑定优先级

优先级是new 调用 > call、apply、bind 调用 > 对象上的函数调用 > 普通函数调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

var name = 'window';
var person = {
name: 'person',
}
var doSth = function(){
console.log(this.name);
return function(){
console.log('return:', this.name);
}
}
var Student = {
name: '若川',
doSth: doSth
}
// 普通函数调用
doSth(); // window
// 对象上的函数调用
Student.doSth(); // '若川'
// call、apply 调用
Student.doSth.call(person); // 'person'
new Student.doSth.call(person) // VM36:21 Uncaught TypeError: Student.doSth.call is not a constructor

最后一个执行报错
这是因为函数内部有两个不同的方法:[[Call]]和[[Constructor]]。
当使用普通函数调用时,[[Call]]会被执行。当使用构造函数调用时,[[Constructor]]会被执行。call、apply、bind和箭头函数内部没有[[Constructor]]方法。

总结

  • new 调用:绑定到新创建的对象,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。
  • call 或者 apply( 或者 bind) 调用:严格模式下,绑定到指定的第一个参数。非严格模式下,null和undefined,指向全局对象(浏览器中是window),其余值指向被new Object()包装的对象。
  • 对象上的函数调用:绑定到那个对象。
  • 普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象。

参考