《你不知道的JavaScript》系列二--------this绑定详解
关于this的误解
1.指向自身
不管是JavaScript新手开发者还是之前已经接触过其他语言的开发者,都很容易将this理解为指向函数自身。下面简单的例子将证明this远远不止字面意思的这么简单:
function foo(num){
console.log("foo:"+num);
//希望用count来记录foo被调用的次数
this.count++;
}
foo.count = 0;
for(let i=0;i<5;i++){
foo(i);
}
//foo:0
//foo:1
//foo:2
//foo:3
//foo:4
//见证奇迹的时候到了
console.log(foo.count);//0 ———— WTF!!?
从上面代码的输出可以看出从字面意思来理解this是错误的,那如果我增加的count属性和预期的不一样,那我增加的count哪去了?看看下图就一目了然了
2.指向函数的作用域
需要明确的是,this在任何情况下都不指向函数的词法作用域。在JavaScript内部,作用域确实是与对象类似,可见的标识符都是它的属性,但是作用域“对象”无法通过JavaScript代码访问,它存在与JavaScript引擎内部————《你不知道的JavaScript》
function foo(){
var a=2;
this.bar();
}
function bar(){
console.log(this.a);
}
foo();//ReferenceError: a is not defined
这段代码试图使用this来联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里面的变量a。这是不可能实现的,我们不能使用this来引用一个词法作用域内部的东西。
每当你想要把词法作用域和this的查找混合使用时,一定要提醒自己,这是无法实现的
this到底是什么
当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。
调用栈:为了到达当前执行位置所调用的所有函数
调用位置:在当前正在执行的函数的前一个调用中
this的四条绑定规则
1.默认绑定
独立函数调用时使用默认绑定规则,绑定到全局对象(非严格模式)或者undefined(严格模式)
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 2
分析上述代码:
①:在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,一次只能使用默认绑定,无法使用其他规则。
②:上述代码执行在非严格模式下,因此this绑定到了全局对象上
③:this.a被解析为全局变量a,打印2
Tips:只有在非严格模式下调用foo(),默认绑定才能绑定到全局对象
2.隐式绑定
当调用位置有上下文对象,或者说是否被某个对象拥有或者包含时,考虑隐式绑定。
这个说法非常抽象,具体还是需要通过代码来进行讲解:
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
obj.foo(); // 2
在上述代码中,调用位置会使用obj上下文来引用函数,因此隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因此this.a和obj.a是一样的
Tips:对象属性引用链中只有最顶层或者说最后一层会影响调用位置
function foo(){
console.log(this.a);
}
var obj2 = {
a:333,
foo:foo
};
var obj1 = {
a:222,
obj2:obj2
};
obj1.obj2.foo(); // 333
隐式丢失
隐式丢失是非常常见的this绑定问题,也就是说被隐式绑定的函数丢失绑定对象,应用默认绑定,将this绑定到全局对象(严格模式)或者undefined(非严格模式)上;
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo;
var a = "糟糕,我是全局对象";
bar(); // "糟糕,我是全局对象"
虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。值得注意的是,参数传递就是一种隐式赋值,因此在回调函数中,绑定的this也是全局对象。
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
var a = "糟糕,我是全局对象";
setTimeout(obj.foo,1000); // "糟糕,我是全局对象"
3.显式绑定
使用call()、apply()直接指定this的绑定对象,称之为显式绑定。
显式绑定的两个应用:
(1)硬绑定
观察以下代码,无论之后如何调用函数bar,它总会手动在obj上调用foo
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
var bar = function(){
foo.call(obj);
}
bar(); // 2
setTimeout(bar,100); // 2
//硬绑定的bar不可能再修改它的this
bar.call(window); // 2
由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Funtion.prototype.bind,bind()会返回一个硬绑定的新函数,它会将参数设置为this的上下文。
(2)API调用的上下文
第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常称之为“上下文”(context),作用和bind()一样,确保回调函数可以使用指定的this
function foo(){
console.log(el,this.id);
}
var obj = {
id : "awesome";
}
//调用foo的时候把this绑定到obj上
[1,2,3].forEach(foo,obj);
//1 awesome 2 awesome 3 awesome
4.new绑定
使用new来调用函数,会自动执行下面的操作:
- 创建一个全新的对象
- 这个新对象会被执行原型连接
- 这个新对象会绑定到函数调用的this
- 如果这个函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
使用new来调用foo()时,我们会构建一个新对象并将它绑定到foo()调用的this上。
5.四条绑定规则的优先级
优先级:new绑定>显式绑定>隐式绑定>默认绑定
6.绑定例外
(1)被忽略的this
如果将null或者undefined作为this的绑定对象传入call、apply或者bind中时,这些值会被忽略,应用默认绑定规则
(2)间接引用
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;
p.foo(); // 2
赋值表达式p.foo = o.foo;的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo。因此此处应用的是默认绑定
(3)箭头函数
箭头函数不适用this的四种绑定规则,而是根据外层作用域来决定this,并且箭头函数的绑定无法修改,new也不行!
function foo(){
return (a) => {
//this继承自foo()的外层作用域,即window
console.log(this.a);
}
}
var obj1 = {
a:2
}
var obj2 = {
a:3
}
var bar = foo.call(obj1);
bar.call(obj2); // 2
7.总结
-
函数是否在new中调用?如果是的话this绑定的是新创建的对象
var bar = new foo(); -
函数是否通过call、apply显式绑定或者bind硬绑定?如果是的话,this绑定的是指定对象
var bar = foo.call(obj2); -
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是上下文对象
var bar = obj1.foo() -
如果以上都不是的话,使用默认绑定。在严格模式下,绑定到undefined,非严格模式下,绑定到全局对象
var bar = foo()
需要注意的是有些调用,特别是赋值,可能会在无意中使用默认绑定标准。并且箭头函数不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this。
8.课后习题
1.
const object = {
message: 'Hello, World!',
getMessage() {
const message = 'Hello, Earth!';
return this.message;
}
};
console.log(object.getMessage()); // ??
2.
function Pet(name) {
this.name = name;
this.getName = () => this.name;
}
const cat = new Pet('Fluffy');
console.log(cat.getName());
const { getName } = cat;
console.log(getName());
3.
const object = {
message: 'Hello, World!',
logMessage() {
console.log(this.message);
}
};
setTimeout(object.logMessage, 1000);
4.
如何调用logMessage函数,让它打印 "Hello, World!" ?
const object = {
message: 'Hello, World!'
};
function logMessage() {
console.log(this.message);
}
5.
const object = {
who: 'World',
greet() {
return `Hello, ${this.who}!`;
},
farewell: () => {
return `Goodbye, ${this.who}!`;
}
};
console.log(object.greet());
console.log(object.farewell());
6.
var length = 4;
function callback() {
console.log(this.length);
}
const object = {
length: 5,
method(callback) {
callback();
}
};
object.method(callback, 1, 2);
7.
var length = 4;
function callback() {
console.log(this.length);
}
const object = {
length: 5,
method() {
arguments[0]();
}
};
object.method(callback, 1, 2);
答案
-
'Hello, World!'隐式调用,此时的this绑定object上
-
'Fluffy'和'Fluffy'当函数作为构造函数
new Pet('Fluffy')调用时,构造函数内部的this等于构造的对象Pet构造函数中的this.name = name表达式在构造的对象上创建name属性。this.getName = () => this.name在构造的对象上创建方法getName。而且由于使用了箭头函数,箭头函数内部的this值等于外部作用域的this值, 即Pet。调用
cat.getName()以及getName()会返回表达式this.name,其计算结果为'Fluffy'。 -
undefinedobject.logMessage作为回调函数实际上引用的是logMessage函数本身,因此此时的logMessage()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。
-
显式绑定or硬绑定
message: 'Hello, World!' }; function logMessage() { console.log(this.message); // logs 'Hello, World!' } // Using func.call() method logMessage.call(object); // Using func.apply() method logMessage.apply(object); // Creating a bound function const boundLogMessage = logMessage.bind(object); boundLogMessage(); -
'Hello, World!'和'Goodbye, undefined!'当调用
object.greet()时,在greet()方法内部,this值等于 object,因为greet是一个常规函数。因此object.greet()返回'Hello, World!'。但是
farewell()是一个箭头函数,箭头函数中的this值总是等于外部作用域中的this值。farewell()的外部作用域是全局作用域,它是全局对象。因此object.farewell()实际上返回'Goodbye, ${window.who}!',它的结果为'Goodbye, undefined!'。 -
4
callback()是在method()内部使用常规函数调用来调用的。由于在常规函数调用期间的this值等于全局对象,所以this.length结果为window.length。第一个语句
var length = 4,处于最外层的作用域,在全局对象window上创建一个属性length。 -
3
将callback隐式赋值给了argument[0],那么调用用的是argument的length