从开始接触this到真正理解this之前,可能会对this有一些误解, 如:
第一种常见的倾向是认为this指向函数自己;
第二常见的对this的含义的误解,是它不知怎的指向了函数的作用域。
明确地说,this不会以任何方式指向函数的 词法作用域。作用域好像是一个将所有可用标识符作为属性的对象,这从内部来说是对的。但是JavasScript代码不能访问作用域“对象”。它是 引擎 的内部实现。
什么是this?
上面已经列举了各种不正确的臆想,现在来看this机制是到底如何真正工作的。
我们早先说过,this不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this绑定和函数声明的位置无关,反而和函数被调用的方式有关。
当一个函数被调用时,会建立一个活动记录,也称为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。
简单来说, 大概有4种:
- 默认绑定
- 隐式绑定
- 显示绑定
- new绑定
特别要注意隐式赋值,比如传参 、赋值的时候,实际上对象和函数是两个地址, 如果不加上对象的调用,单独调用实际上就是在全局环境调用函数。下面一一道来。
1. 默认绑定
最常见的情况:独立函数调用。可以认为这种this规则是在没有其他规则适用时的默认规则。
考虑如下代码:
function foo() {
console.log(this.a);
}
var a = 1; // 全局变量
foo(); // 1
当foo()被调用时,this.a解析的是全局变量a。为什么?因为在这种情况下,foo()是在全局环境下被调用的,对此方法调用的this实施了 默认绑定,所以使this指向了全局对象。
如果在严格模式下,那么对于 默认绑定 来说全局对象是不合法的,所以this将被设置为undefined。
function foo() {
"use strict";
console.log(this.a);
}
var a = 1; // 全局变量
foo(); // TypeError: 'this' is undefined
2. 隐式绑定(Implicit Binding)
另一种要考虑的规则是:调用函数的是一个环境对象(context object)。
考虑如下代码:
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
foo: foo;
};
obj.foo(); // 1
首先,注意foo()被声明然后作为引用属性添加到obj上的方式。无论foo()是否一开始就在obj上被声明,还是后来作为引用添加(如上面代码所示),都是这个 函数 被obj所“拥有”或“包含”。
只有对象属性引用链的最后一层是影响调用点的。比如:
function foo() {
console.log(this.a);
}
var obj1 = {
a: 1,
foo: foo;
};
var obj2 = {
a: 2,
obj1: obj1;
};
obj2.obj1.foo(); // 1
隐式丢失(Implicitly Lost)
this绑定最常让人沮丧的事情之一,就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据strict mode的状态,结果不是全局对象就是undefined。
考虑如下代码:
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
foo: foo;
};
var a = 'global';
var fn = obj.foo; // 将函数引用复制给fn
fn(); // 'global'
尽管fn似乎是obj.foo的引用,但实际上它只是另一个foo自己的引用而已。另外,起作用的调用位置是fn(),一个直白,毫无修饰的调用,因此 默认绑定 适用于这里。
这种情况发生的更加微妙,更常见,更意外的方式,是当我们考虑传递一个回调函数时:
function foo() {
console.log(this.a);
}
function foo2(fn) {
// 这里 fn 仅仅是 foo 的一个 引用
fn(); // 调用位置
}
var obj = {
a: 1,
foo: foo;
};
var a = 'global';
var fn = obj.foo; // 将函数引用复制给fn
foo2(obj.foo); // 'global'
js里参数都是按值传递的, 所以在执行foo2(obj.foo)的时候,所以obj.foo将foo的引用值传给了fn,之后运行回调函数fn(), 结果跟上一个例子一样。
那么如果接收你所传递回调的函数不是你的,而是语言内建的呢?没有区别,同样的结果。
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
foo: foo;
};
var a = 'global';
setTimeout(obj.foo, 1000) // 'global'
如我们刚刚看到的,我们的回调函数丢掉他们的this绑定是十分常见的事情。
不管哪一种意外改变this的方式,你都不能真正地控制你的回调函数引用将如何被执行,所以你(还)没有办法控制调用点给你一个故意的绑定。我们很快就会看到一个方法,通过 固定 this来解决这个问题。
显示绑定(Explicit Binding)
JavaScript语言中的“所有”函数都有一些工具(通过他们的[[Prototype]])可以用于这个任务。特别是,函数拥有call(..)和apply(..)方法。绝大多数被提供的函数,当然还有你将创建的所有的函数,都可以访问call(..)和apply(..)。
call(..)和apply(..)接收的第一个参数都是一个用于绑定this的对象,之后使用这个指定的this来调用函数。因为你已经直接指明你想让this是什么,所以我们称这种方式为 显示绑定(explicit binding)。
考虑如下代码:
function foo() {
console.log(this.a);
}
var obj = {
a: 1
};
var a = 'global';
foo.call(obj); // 1
通过foo.call(..)使用 明确绑定 来调用foo,允许我们强制函数的this指向obj。
如果你传递一个简单原始类型值(string,boolean,或 number类型)作为this绑定,那么这个原始类型值会被包装在它的对象类型中(分别是new String(..),new Boolean(..),或new Number(..))。这通常称为“boxing(封箱)”。
注: 就this绑定的角度讲,call(..)和apply(..)是完全一样的。它们确实在处理其他参数上的方式不同。
不幸的是,单独依靠 明确绑定 仍然不能为我们先前提到的问题提供解决方案,也就是函数“丢失”自己原本的this绑定,或者被第三方框架覆盖,等等问题。
硬绑定(Hard Binding)
考虑这段代码:
function foo(sth) {
console.log(this.a, sth);
return this.a + sth;
}
var obj = {
a: 1;
}
var bar = function() {
return foo.apply(obj);
};
bar(); // 1
setTimeout(bar, 100); // 1
// bar 将 foo的this应绑定到 obj
bar.call(window); // 1
代码中, 创建了一个函数bar(),在它的内部手动调用foo.call(obj),由此强制this绑定到obj并调用foo。无论你过后怎样调用函数bar,它总是会使用obj调用foo。这种绑定即明确又坚定,所以我们称之为 硬绑定(hard binding)。
用 硬绑定 将一个函数包装起来的最典型的方法,是为所有传入的参数和传出的返回值创建一个通道:
function foo(sth) {
console.log(this.a, sth);
return this.a + sth;
}
var obj = {
a: 1;
}
var bar = function() {
return foo.apply(obj, arguments);
};
var baz = bar(2); // 1 2
console.log(baz); // 3
另一种表达这种模式的方法是创建一个可复用的帮助函数:
function foo(sth) {
console.log(this.a, sth);
return this.a + sth;
}
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments);
};
}
var obj = {
a: 1;
}
var bar = bind(foo, obj);
var baz = bar(2); // 1 2
console.log(baz); // 3
硬绑定 是一个很常用的模式,它已作为ES5的内建工具提供:Function.prototype.bind,像这样使用:
function foo(sth) {
console.log(this.a, sth);
return this.a + sth;
}
var obj = {
a: 1;
}
var bar = foo.bind(obj);
var baz = bar(2); // 1 2
console.log(baz); // 3
bind(..)返回一个硬编码的新函数,它使用你指定的this环境来调用函数。
new绑定(new Binding)
- 一个全新的对象会凭空创建(就是被构建)
- 这个新构建的对象会被接入原形链([[Prototype]]-linked)
- 这个新构建的对象被设置为函数调用的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绑定(new binding)。
总结: this绑定基本有4中可能:
1. 默认绑定,指向顶层对象,在严格模式下, 指向undefined,混杂模式下指向全局对象;
2. 隐式绑定,考虑调用位置是否有上下文,或者说是否被某个而对象拥有或者包含,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。此时,存在的一个最大的问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定。
3. 显示绑定,call、apply、bind 绑定,将传给他们的第一个参数作为绑定this的对象。(硬绑定会通过Function.prototype.bind()创建一个新的包装函数,这个函数会忽略它当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上)。
4. new绑定,this指向新创建的实例对象。
bind的简单实现:
function bind(fn, obj){
return function() {
fn.apply(obj, arguments);
};
}
上述四种绑定的优先级:
1. 默认绑定的优先级最低。
2. 显示绑定高于隐式绑定;
3. new绑定高于显示绑定;
所以有显示 new绑定 > 显示绑定 > 隐式绑定 > 默认绑定 。
以上是学习总结,请多指正!