this 豁然开朗的四种规则
调用点(call-site)
调用点:“找到一个函数是在哪里被调用的”,但不总是那么简单,比如某些特定的编码模式会使 真正的 调用点变得不那么明确。
调用栈(call-stack)
调用栈:使我们到达当前执行位置而被调用的所有方法的堆栈。
我们来展示一下调用栈和调用点:
function baz() {
// 调用栈是: `baz`
// 我们的调用点是global scope(全局作用域)
console.log( "baz" );
bar(); // <-- `bar`的调用点
}
function bar() {
// 调用栈是: `baz` -> `bar`
// 我们的调用点位于`baz`
console.log( "bar" );
foo(); // <-- `foo`的call-site
}
function foo() {
// 调用栈是: `baz` -> `bar` -> `foo`
// 我们的调用点位于`bar`
console.log( "foo" );
}
baz(); // <-- `baz`的调用点
1. this what?
this是JavaScript的关键字之一。它是 对象 自动生成的一个内部对象,只能在 对象 内部使用。随着函数使用场合的不同,this的值会发生变化。
this指向什么,完全取决于 什么地方以什么方式调用,而不是 创建时。(就是找到它的 调用点!)
2. this 绑定的四种规则
-2.1 默认绑定(Default Binding)
第一种规则来源于函数调用的最常见的情况:独立函数调用。
考虑这个代码段:
function foo() {
var a = 1;
console.log( this.a );
}
var a = 2;
foo(); // 2
我们看到当 foo() 被调用时,this.a解析为我们的全局变量a。为什么?因为在这种情况下,对此方法调用的this实施了 默认绑定,所以使this指向了全局对象。
这种就是典型的 默认绑定,我们看看foo调用的位置,”光杆司令“,像 这种直接使用而不带任何修饰的函数调用 ,就默认且只能应用 默认绑定。
那默认绑定到哪呢,一般是window上,严格模式下 是undefined。因为如果 strict mode 在这里生效,那么对于 默认绑定 来说全局对象是不合法的,所以this将被设置为undefined
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: `this` is `undefined`
一个细节问题:
即便所有的this绑定规则都是完全基于调用点,如果foo()的 内容 没有在strint mode下执行,对于 默认绑定 来说全局对象是 唯一 合法的;foo()的调用点的strict mode状态与此无关。
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
-2.2 隐含绑定(Implicit Binding)
调用点是否有一个环境对象(context object),也称为拥有者(owning)或容器(containing)对象,虽然这些名词可能有些误导人。
看如下代码:
function foo(){
console.log(this.a);
}
var obj = {
a : 10,
foo : foo
}
foo(); // undefined
obj.foo(); // 10
foo(); 就是上面的默认绑定,等价于打印 window.a,故输出 undefined 。
obj.foo(); 就是隐含绑定。
函数foo执行的时候有了上下文对象,即 obj。这种情况下,函数里的this默认绑定为上下文对象,等价于打印obj.a,故输出10 。
注意:只有对象属性引用链的最后一层是影响调用点的,如下代码:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐含绑定的限制(Implicitly Lost)
隐性绑定中有一个致命的限制,就是上下文必须包含我们的函数 ,例:var obj = { foo : foo },如果上下文不包含我们的函数用隐性绑定明显是要出错的,不可能每个对象都要加这个函数 ,那样的话扩展,维护性太差了,我们接下来聊的就是直接 给函数强制性绑定this.
- 2 .3 显性绑定
call、 apply 、 bind
函数 call 和 apply,它们的作用都是改变函数的this指向,第一个参数都是 设置this对象。
两个函数的区别:
- call从第二个参数开始所有的参数都是 原函数的参数。
- apply只接受两个参数,且第二个参数必须是数组,这个数组代表原函数的参数列表。 例如:
function foo(a,b){
console.log(a+b);
}
foo.call(null,'海洋','饼干'); // 海洋饼干 这里this指向不重要就写null了
foo.apply(null, ['海洋','饼干'] ); // 海洋饼干
除了 call,apply函数以外,还有一个改变this的函数 bind ,它和call,apply都不同。
bind 只有一个函数,且不会立刻执行,只是将一个值绑定到函数的this上,并将绑定好的函数返回。例:
function foo(){
console.log(this.a);
}
var obj = { a : 10 };
foo = foo.bind(obj);
foo(); // 10
开始我们的显性绑定
function foo(){
console.log(this.a);
}
var obj = {
a : 10 //去掉里面的foo
}
foo.call(obj); // 10
我们将隐性绑定例子中的 上下文对象 里的函数去掉了,显然现在不能用 上下文.函数 这种形式来调用函数,大家看代码里的显性绑定代码foo.call(obj),看起来很怪,和我们之前所了解的函数调用不一样。
其实call 是 foo 上的一个函数,在改变this指向的同时执行这个函数。
-2.4 new 绑定
js中的只要用new修饰的 函数就是’构造函数’,准确来说是 函数的构造调用,因为在js中并不存在所谓的’构造函数’。
用 new 做到函数的构造调用后,js帮我们做了什么工作呢: 1. 创建一个新对象。 2. 把这个新对象的proto属性指向 原函数的prototype属性。或者说是将构造函数的作用域给这个新对象。(即继承原函数的原型,因此 this 就指向了这个新对象); 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绑定(new binding)。
- 2 .5 this绑定优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
-3. 总结
1. 如果函数被new 修饰
this绑定的是新创建的对象,例:var bar = new foo(); 函数 foo 中的 this 就是一个叫foo的新创建的对象 , 然后将这个对象赋给bar , 这样的绑定方式叫 new绑定 .
2. 如果函数是使用call,apply,bind来调用的
this绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是 obj , 这样的绑定方式叫 显性绑定 .
3. 如果函数是在某个 上下文对象(容器) 下被调用
this绑定的是那个上下文对象,例 : var obj = { foo : foo }; obj.foo(); foo 中的 this 就是 obj . 这样的绑定方式叫 隐性绑定 .
4. 如果都不是,即使用默认绑定
例:function foo(){…} foo() ,foo 中的 this 就是 window.(严格模式下默认绑定到undefined). 这样的绑定方式叫 默认绑定 .
例题如下:
var x = 10;
var obj = {
x: 20,
f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
x: 30,
f: obj.f
}
obj.f(); // 隐含绑定
bar(); // 默认绑定
obj2.f(); // 隐含绑定
答案:20 10 30。