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实现的显示绑定,例如:
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干了什么。
- 创建一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 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 小结
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。