关于this...

75 阅读5分钟

1 关于this

1.1 为什么要用this?

我们先看代码:(隐式传参)

function foo() {
    return this.personNum;
}
let obj = {
    personNum: 3,
};
console.log(foo.call(obj)); // 3

如果我们不用this的话我们要实现上面效果:(显式传参)

function foo(obj) {
    return obj.personNum;
}
let obj = {
    personNum: 3,
};
console.log(foo(obj));

随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。

2 this的误解

2.1 误区一 this指向自身

程序员很容易将this当成英语的this来理解,指向自身。实际上this并不是指向的自身。

例如:

function foo(num) {
    console.log("foo: " + num);
    // 记录 foo 被调用的次数
    this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}
console.log(this.count);
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// undefined

console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然 是 0。显然从字面意思来理解 this 是错误的。

这里的this实际上是指的window。

2.2 误区二 this指向它的作用域

有的开发者认为,this是指向函数的作用域。这个说法并不完全对,只有在某种情况下才是正确的,其他情况都是错误的。

需要明确的是,this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用 域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。

例如:

function bar() {
    console.log(this.a);
}
function foo() {
    var a = 2;
    this.bar();
}
foo(); // ReferenceError: a is not defined

实际上,这里的this并没有指向到我们认为的bar的被调用时的作用域内a,而是指向了全局作用域window。

那么有人可能会问了,this到底是如何绑定的呢,到底指向了什么呢?

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

3 this的绑定

3.1 默认绑定

先看代码:

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

函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。但是需要注意的是:在严格模式下,全局对象是无法使用默认绑定的,this会指向undefined。

这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只 有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo() 的调用位置无关:

function foo() {
    "use strict";
    console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined
/**------------------分割线-----------------*/
function foo() {
    console.log(this.a);
}
var a = 2;
(function () {
    "use strict";
    foo(); // 2
})();

3.2 隐式绑定

先看代码:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo,
};
foo(); // undefind
var bar = obj.foo();
bar() // undefind // 注意:这里村子一个隐式绑定丢失的问题
// 虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
obj.foo(); // 2

首先需要注意的是 foo() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。 但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象。

然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥 有”或者“包含”它。

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

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

3.3显示绑定

这里的显示绑定和call、bind、apply相关,三者的第一个参数都是一个对象,并将this指向这个对象。需要注意的是bind的绑定并非立即执行的,需要再调用一次。

三者的区别和具体用法实现参考

手写call、apply、bind实现及详解

当然,还有一些其他的隐式通过call或者apply实现的显示绑定,例如:

function foo(el) {
    console.log(el, this.id);
}
var obj = {
    id: "awesome",
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach(foo, obj);
// 1 awesome 2 awesome 3 awesome

3.4 new绑定

要理解new绑定,我们首先要知道new干了什么。

  1. 创建一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

关于第二点,后续在写原型及原型链时解释。

先看代码:

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

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

既然this有这么多的绑定规则,那他们的优先级是什么样的呢?

4 优先级

直接上代码测试:

4.1 显式绑定和隐式绑定

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

结论:显然,显式绑定的优先级高于隐式绑定。

4.2 new绑定和隐式绑定

function foo(something) {
	this.a = something;
}
var obj1 = {
	foo: foo,
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3
var bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4

结论:显然,new绑定的优先级高于隐式绑定。

4.3 new绑定和显式绑定

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3

结论:显然,new绑定会替换掉显式绑定的this。

5 小结

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