this
关于this,其实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 );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log( foo.count );// 0
console.log(count); //NaN
上述代码中,我们企图用this.count指向foo函数本身,以此在其作用域创建一个count属性。但是是代码的实际结果告诉我们,这样写并不能做到我们所想要的结果。
这种写法,最后会使代码在无意中创建了一个全局变量count,其值为NaN。
如果要从函数对象内部引用其自身,只使用
this是不够的。一般而言,需要一个指向函数对象的词法标识符来引用它。
对于上述情况,其实有许多解决方式。但是既然我们的主题是this,那就用this的方式来解决。
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
// 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 call(..) 可以确保 this 指向函数对象 foo 本身
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log( foo.count ); // 4
这个方式,是强制this指向foo函数对象。
this的作用域
对于this,还有一种误解。就是this指向函数的作用域。
但是实际上,this在任何情况下都不指向函数的词法作用域。
举个例子:
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
上述方法,其实在有一定代码基础的人眼里,会觉得很奇怪。因为我们要调用bar()方法的话,这个this实际上是多余的。
并且,this这个操作并不能跨作用域引用变量。
切记,不要将
this和词法作用域混合使用。
What is 'this'
在标题上玩了一下英语的谐音梗,哈哈哈,希望不要介意。
上面讲述了那些之后,我们需要了解到的是,this到底是一个什么样的机制。
其实,this的绑定和函数声明的位置,没有半毛钱关系,半分钱也没有。它只取决于函数的调用方式。
当一个函数被调用时,会创建一个执行上下文。它会包含函数在哪里被调用、调用方法和传参等信息。this就是其中的一个属性,在函数执行过程中会用到。
this的全面解析
绑定规则
在了解this的绑定规则之前,我们需要知道了解调用位置。
调用位置实际上就是指我们调用对象的词法标识符的位置。
this的绑定规则有四条。
默认绑定
最常用的函数调用类型:独立函数调用。这种方法就可以使用默认绑定。
function foo(){
console.log(this.a);
}
var a = 2;
foo(); //2
我们可以发现,我们在调用foo()时,其内部的this指向了全局对象,这是因为应用了this的默认绑定。
代码中,
foo()是直接使用不带任何修饰的函数引用进行调用,因此只能使用默认绑定,无法应用其他规则。
需要注意的是,上述情况只有在非严格模式下 使用。
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
隐式绑定
隐式的使用,需要考虑调用位置是否有上下文对象。
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
obj.foo();
我们在使用了obj.foo()的方式来调用函数时,函数foo()其实不属于obj对象。但是这种方式,让函数的落脚点在obj对象内,this指向的是obj对象内,因此可以调用到obj内部的变量。
隐式丢失
在使用this绑定的时候,有个常见的问题,就是被隐式绑定的函数会丢失绑定对象。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global
上述代码,在使用过程中,使用bar引用了obj.foo,但是实际上,引用的还是函数本身。所以,此时 bar()其实是一个不带任何修饰的函数调用 ,因此应用了默认绑定。
还有另一种情况:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
我们在使用
this的时候,应该严格注意避开类似情况。因为我们无法控制回调函数的执行方式,从而导致无法正确的使用我们想要使用的绑定方式。
显式绑定
JavaScript 中的“所有”函数都有一些有用的特性,可以用来解决这个问题。具体点说,可以使用函数的 call(..) 和apply(..) 方法。严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。
显示绑定主要分为两种。
-
硬绑定 硬绑定其实就是使用
call(..)和apply(..)方法。call: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 ); // 2apply:function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a:2 }; var bar = function() { return foo.apply( obj, arguments ); }; var b = bar( 3 ); // 2 3 console.log( b ); // 5
由于硬绑定是一种非常常用的模式,所以在
ES5中提供了内置的方法Function.prototype. bind,它的用法如下:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。
API调用的上下文
第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this 。
实际上该种方法也是通过
call()或者apply()来实现的显式绑定,只不过不需要我们自己来写。
new绑定
在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用 new 初始化类时会
调用类中的构造函数。通常的形式是这样的:
something = new MyClass(..);
JavaScript 也有一个 new 操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都认为 JavaScript 中 new 的机制也和那些语言一样。然而,JavaScript 中 new 的机制实际上和面向类的语言完全不同。
实际上, 在
JavaScript中并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用 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
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的 顺序来进行判断:
- 函数是否在
new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo()
- 函数是否通过
call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,
this绑定的是那个上 下文对象。
var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到
undefined,否则绑定到全局对象。
var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。
绑定例外
凡是规则,都会有出现意外的情况。this绑定也不例外。
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
当我们把null或者undefined传入call、apply或者bind时,这些值在调用时会被忽略,从而去应用默认绑定规则。
间接引用
当我们创建一个函数的“间接引用”时,调用这个函数就会应用默认绑定规则。
简介引用最容易在赋值时发生。
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绑定对象的并不是调用位置是否处于严格模式,而是 函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。
软绑定
前面有提到一种绑定方式,叫做硬绑定,所以一般相对的,其实也有一种叫做软绑定的方式。
使用硬绑定会降低函数的灵活性,之后无法再使用隐式绑定或者显式绑定来修改
this。但是软绑定 ,可以保留隐式绑定和显示绑定修改this的能力。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
this词法
在ES6中,介绍了一种特殊的函数类型:箭头函数。
箭头函数并不是使用 function关键字定义的,而是使用被称为“胖箭头”的操作符 => 定
义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决
定 this。
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
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 ,箭头函数的绑定无法被修改。
箭头函数可以像
bind(..)一样确保函数的this被绑定到指定对象,此外,其重要性还体 现在它用更常见的词法作用域取代了传统的this机制。
总结
要判断this绑定,就需要先找到函数的直接调用位置。
之后根据规则来判断this的绑定对象:
1. `new`调用
2.call、apply或者bind
3.上下文对象调用
4.默认绑定
ES6中的箭头函数并不会使用四条标准的绑定规则。而是根据当前的词法作用域来决定this。箭头函数会继承外层函数调用的this绑定。