深入浅出 JavaScript 中的 this

笔者最近在看 你不知道的JavaScript上卷,里面关于

this
的讲解个人觉得非常精彩。
JavaScript
中的
this
算是一个核心的概念,有一些同学会对其有点模糊和小恐惧,究其原因,现在对
this
讨论的文章很多,让我们觉得
this
无规律可寻,就像一个幽灵一样

如果你还没弄懂

this
,或者对它比较模糊,这篇文章就是专门为你准备的,如果你相对比较熟悉了,那你也可以当做复习巩固你的知识点

本篇文章,算是一篇读书笔记,当然也加上了很多我的个人理解,我觉得肯定对大家有所帮助

执行上下文

在理解

this
之前,我们先来看下什么是执行上下文

简而言之,执行上下文是评估和执行

JavaScript
代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行

JavaScript 中有三种执行上下文类型

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的
    window
    对象(浏览器的情况下),并且设置
    this
    的值等于这个全局对象。一个程序中只会有一个全局执行上下文
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个
  • eval
    函数执行上下文 — 执行在
    eval
    函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用
    eval
    ,所以在这里我不会讨论它

这里我们先得出一个结论, 非严格模式和严格模式中 this 都是指向顶层对象(浏览器中是window)

 console.log(this === window); // true
'use strict'
console.log(this === window); // true
this.name = 'vnues';
console.log(this.name); // vnues

后面我们的讨论更多的是针对函数执行上下文

this 到底是什么?为什么要用 this

this
是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件

牢记:

this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。

this
就是记录的 其中一个属性,会在函数执行的过程中用到

看个实例,理解为什么要用

this
,有时候,我们需要实现类似如下的代码:

 function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify(context);
console.log(greeting);
}
var me = {
name: "Kyle"
};
speak(me); //hello, 我是 KYLE

这段代码的问题,在于需要显示传递上下文对象,如果代码越来越复杂,这种方式会让你的代码看起来很混乱,用

this
则更加的优雅

 var me = {
name: "Kyle"
};

function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call(this);
console.log(greeting);
}
speak.call(me); // Hello, 我是 KYLE

this 的四种绑定规则

下面我们来看在函数上下文中的绑定规则,有以下四种

  • 默认绑定
  • 隐式绑定
  • 显式绑定
  • new
    绑定

默认绑定

最常用的函数调用类型:独立函数调用,这个也是优先级最低的一个,此事

this
指向全局对象。注意:如果使用严格模式(
strict mode
),那么全局对象将无法使用默认绑定,因此
this
会绑定 到
undefined
,如下所示

 var a = 2;  //  变量声明到全局对象中
function foo() {
console.log(this.a); // 输出 a
}

function bar() {
'use strict';
console.log(this); // undefined
}
foo();
bar();

隐式绑定

还可以我们开头说的:

this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

先来看一个例子:

 function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2

当调用

obj.foo()
的时候,
this
指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的
this
绑定到这个上下文对象。因为调 用
foo()
this
被绑定到 obj,因此 this.a 和 obj.a 是一样的

记住: 对象属性引用链中只有最顶层或者说最后一层会影响调用位置

 function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则

 function foo() {
console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则

赋值表达式

p.foo = o.foo
的返回值是目标函数的引用,因此调用位置是
foo()
而不是
p.foo()
或者
o.foo()
。根据我们之前说过的,这里会应用默认绑定

显示绑定

在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把

this
间接(隐式)绑定到这个对象上。 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么
做呢?

Javascript
中提供了
apply
call
bind
方法可以让我们实现

不同之处在于,

call()
apply()
是立即执行函数,并且接受的参数的形式不同:

  • call(this, arg1, arg2, ...)
  • apply(this, [arg1, arg2, ...])

bind()
则是创建一个新的包装函数,并且返回,而不是立刻执行

  • bind(this, arg1, arg2, ...)

看如下的例子:

 function foo(b) {
console.log(this.a + '' + b);
}
var obj = {
a: 2,
foo: foo
};
var a = 1;
foo('Gopal'); // 1Gopal
obj.foo('Gopal'); // 2Gopal
foo.call(obj, 'Gopal'); // 2Gopal
foo.apply(obj, ['Gopal']); // 2Gopal
let bar = foo.bind(obj, 'Gopal');
bar(); // 2Gopal

被忽略的 this

如果你把

null
或者
undefined
作为
this
的绑定对象传入
call
apply
或者
bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则

 function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2

利用这个用法使用

apply(..)
来“展开”一个数组,并当作参数传入一个函数。
类似地,
bind(..)
可以对参数进行柯里化(预先设置一些参数)

 function foo(a, b) {
console.log("a:" + a + ", b:" + b);
}
// 把数组“展开”成参数
foo.apply(null, [2, 3]); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3

new绑定

当我们使用构造函数

new
一个实例的时候,这个实例的
this
指向是什么呢?

我们先来看下使用

new
来调用函数,或者说发生构造函数调用时,会执行什么操作,如下:

  • 创建(或者说构造)一个全新的对象
  • 这个新对象会被执行[[原型]]连接,将对象(实例)的
    __proto__
    和构造函数的
    prototype
    绑定
  • 这个新对象会绑定到函数调用的
    this
  • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

原理实现类似如下:

 function create (ctr) {
// 创建一个空对象
let obj = new Object()
// 链接到构造函数的原型对象中
let Con = [].shift.call(arguments)
obj.__proto__ = Con.prototype
// 绑定this
let result = Con.apply(obj, arguments);
// 如果返回是一个对象,则直接返回这个对象,否则返回实例
return typeof result === 'object'? result : obj;
}

注意:

let result = Con.apply(obj, arguments);
实际上就是指的是新对象会绑定到函数调用的
this

 function Foo(a) {
this.a = a;
}
var bar = new Foo(2);
console.log(bar.a); // 2

特殊情况——箭头函数

我们之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用 这些规则的特殊函数类型:箭头函数

箭头函数不使用

this
的四种标准规则,而是根据定义时候的外层(函数或者全局)作用域来决 定
this
。也就是说箭头函数不会创建自己的
this
,它只会从自己的作用域链的上一层继承
this

 function foo() {
// 返回一个箭头函数
// this 继承自 foo()
return (a) => {
console.log(this.a);
}
};

var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是 3 !

foo()
内部创建的箭头函数会捕获调用时
foo()
this
。由于
foo()
this
绑定到
obj1
bar
(引用箭头函数)的
this
也会绑定到
obj1
,箭头函数的绑定无法被修改。(
new
也不 行!)

总结——this 优先级

判断是否为箭头函数,是则按照箭头函数的规则

否则如果要判断一个运行中函数的

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

  1. new
    调用?绑定到新创建的对象
  2. call
    或者
    apply
    (或者
    bind
    )调用?绑定到指定的对象
  3. 由上下文对象调用?绑定到那个上下文对象
  4. 默认:在严格模式下绑定到
    undefined
    ,否则绑定到全局对象

如下图所示:

参考

  • [译] 理解 JavaScript 中的执行上下文和执行栈

  • 你不知道的JavaScript上卷

标签: Javascript

添加新评论