目录
关于this
一、this
是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
二、当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this
就是这个记录的一个属性,会在函数执行的过程中用到。
this全面解析
调用位置
一、调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
二、调用位置就在当前正在执行的函数的前一个调用中
三、你可以把调用栈想象成一个函数调用链
绑定规则
在函数的执行过程中调用位置如何决定this
的绑定对象?你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。
默认绑定——独立函数调用
一、考虑以下代码,函数调用时应用了this
的默认绑定,因此this
指向全局对象。那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看foo()
是如何调用的。在代码中,foo()
是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this
会绑定到undefined
function foo(){
console.log(this.a);
}
var a = 2;
foo();// 2
二、虽然this
的绑定规则完全取决于调用位置,但是只有foo()
运行在非严格模式下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()
则不影响默认绑定
function foo(){
console.log(this.a);
}
var a = 2;
(function() {
"use strict";
foo();// 2
})();
也就是说,如果被调用的地方是严格模式,不会影响默认绑定,如果在定义的地方是严格模式就会影响
隐式绑定
一、另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含
二、考虑以下代码:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo();// 2
首先需要注意的是foo()
的声明方式,及其之后是如何被当作引用属性添加到obj
中的。但是无论是直接在obj
中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj
对象。然而,调用位置会使用obj
上下文来引用函数,因此你可以说函数被调用时obj
对象“拥有”或者“包含”它。当foo()
被调用时,它的前面确实加上了对obj
的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this
绑定到这个上下文对象。因为调用foo()
时this
被绑定到obj
,因此this.a
和obj.a
是一样的。
三、对象属性引用链中只有上一层或者说最后一层在调用位置中起作用,即this
只想距离foo()
最近的对象obj3
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo();// 42
隐式丢失
一、一个最常见的this
绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this
绑定到全局对象或者undefined
上,取决于是否是严格模式。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo;// 函数别名
var a = "opps, gobal";// a时全局对象的属性
bar();// "opps, gobal"
说明:
- 虽然
bar
是obj.foo
的一个引用,但是实际上,它引用的是foo
函数本身,因此此时的bar()
其实是一个不带任何修饰的函数调用,因此应用了默认绑定。 - 这个例子也说明了函数不属于某一个对象,只能说函数被调用时,某个对象引用了他。
var bar = obj.foo;
传递的是函数本身,而this由调用宿主和环境来决定,函数执行时bar();
执行的时this
指向window
二、无论是把函数传入语言内置的函数还是传入自己声明的函数,参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// fn其实引用的是foo
fn();// 执行位置
}
var obj = {
a: 2,
foo: foo
};
var a = "opps, gobal";// a时全局对象的属性
doFoo(obj.foo);// "opps, gobal"
三、看this
只想,不关心this
属于谁,从哪里来,只关心他在调用时调用者是谁。
四、调用回调函数的函数可能会修改this
。在一些流行的JavaScript库中事件处理器常会把回调函数的this
强制绑定到触发事件的DOM元素上。
显示绑定
一、我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,我们可以使用函数的call(..)
和apply(..)
方法
二、这两个方法的第一个参数是一个对象,是给this
准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this
的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)
、newBoolean(..)
或者new Number(..)
)。这通常被称为“装箱”。
硬绑定
一、显式绑定的一个变种可以解决我们之前提出的丢失绑定问题。
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
我们创建了函数bar()
,并在它的内部手动调用了foo.call(obj)
,因此强制把foo
的this
绑定到了obj
。无论之后如何调用函数bar
,它总会手动在obj
上调用foo
。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
二、硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值
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
三、另一种使用方法是创建一个可以重复使用的辅助函数:
function foo(something) {
console.log(this.a,something);
return this.a + something;
}
function bind(fn, obj){
return funciton() {
return fn.apply(obj, arguments);
}
}
var obj = {
a: 2
};
var bar = bind(foo, obj);
var b = bar(3);// 2,3
console.log(b);// 5
四、由于硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.prototype.bind
。bind(..)
会返回一个硬编码的新函数,它会把你指定的参数设置为this
的上下文并调用原始函数。
五、Function.prototype.bind(..)
会创建一个新的包装函数,这个函数会忽略它当前的this
绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this
上。
API调用的“上下文”
JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context
),其作用和bind(..)
一样,确保你的回调函数使用指定的this
。
new绑定
一、“构造函数”是类中的一些特殊方法,使用new
初始化类时会调用类中的构造函数。
二、在JavaScript中,构造函数只是一些使用new
操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new
操作符调用的普通函数而已。
三、包括内置对象函数在内的所有函数都可以用new
来调用,这种函数调用被称为构造函数调用。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
四、使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行
[[Prototype]]
连接 - 这个新对象会绑定到函数调用的
this
- 如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个新对象。
优先级
一、默认绑定的优先级是四条规则中最低的
二、测试隐式绑定和显式绑定哪个优先级更高:
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
显式绑定优先级更高
三、测试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);// 执行new的时候,this应该指向新创建的实例对象
console.log(obj1.a);// 2
console.log(bar.a);// 4,实例对象身上有a,且值为4,说明this确实指向了新创建的实例对象,new改变了隐式绑定,new绑定比隐式绑定优先级高。
new
绑定比隐式绑定优先级高。
四、测试new绑定和显式绑定的优先级谁高
function foo(something){
this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);// obj1.foo
bar(2)
console.log(obj1.a);// 2
var baz = new bar(3)
console.log(obj1.a);// 2
console.log(baz.a);// 3
new
绑定比显式绑定优先级高:bar
被硬绑定到obj1
上,但是new bar(3)
并没有像我们预计的那样把obj1.a
修改为3。相反,new
修改了硬绑定(到obj1
的)调用bar(..)
中的this
。因为使用了new
绑定,我们得到了一个名字为baz
的新对象,并且baz.a
的值是3。
五、之所以要在new
中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new
进行初始化时就可以只传入其余的参数。bind(..)
的功能之一就是可以把除了第一个参数(第一个参数用于绑定this
)之外的其他参数都传给下层的函数(这种技术称为“部分应用”,是“柯里化”的一种)。
function foo() {
this.val = p1 + p2;
}
var bar = foo.bind(null, "p1");
var baz = new bar("p2");
baz.val;// p1p2
MDN:bind()的另一个简单用法是使一个函数拥有预设的初始函数。只要将这些参数(如果有的话)作为bind()的参数写在this后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在他们后面。
判断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
一、如果你把null
或者undefined
作为this
的绑定对象传入call
、apply
或者bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则
二、那么什么情况下你会传入null
呢?
- 使用
apply(..)
来“展开”一个数组,并当作参数传入一个函数。 bind(..)
可以对参数进行柯里化(预先设置一些参数)
function foo(a, b) {
console.log("a:"+a+",b:"+b);
}
// 把数组展开成参数
foo.apply(null,[2,3]);// a:2,b:3
// 使用bind(...)进行柯里化
var bar = foo.bind(null,2);
bar(3);// a:2,b:3
这两种方法都需要传入一个参数当作this的绑定对象。如果函数并不关心this的话,你仍然需要传入一个占位值,这时null可能是一个不错的选择
三、在ES6中,可以用...
操作符代替apply(..)
来“展开”数组,foo(...[1,2])
和foo(1,2)
是一样的。可惜目前没有柯里化的相关语法,因此还是需要使用bind(..)
四、把null
或者undefined
作为this
的绑定对象传入call
、apply
或者bind
可能会导致许多难以分析和追踪的bug,不建议使用。
更安全的this
一、如果我们在忽略this
绑定时总是传入一个特殊的对象(其实就是一个空对象),把this
绑定到这个对象,那么任何对于this
的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。
二、在JavaScript中创建一个空对象最简单的方法是Object.create(null)
。Object.create(null)
和{}
很像,但是并不会创建Object. prototype
这个委托,所以它比{}
“更空”
function foo(a, b) {
console.log("a:"+a+",b:"+b);
}
var x = Object.create(null);
// 把数组展开成参数
foo.apply(x,[2,3]);// a:2,b:3
// 使用bind(...)进行柯里化
var bar = foo.bind(x,2);
bar(3);// a:2,b:3
间接引用
一、你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生:
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
传递的是引用地址,(p.foo = o.foo)();
等同于foo()
,所以此时应用的是默认绑定。
二、是不是默认绑定要看调用的地方,而默认绑定时this时undefined还是window,要看函数定义的地方,如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。
软绑定
一、使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this。如果可以给默认绑定指定一个全局对象和undefined
以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this
的能力。可以通过一种被称为软绑定的方法来实现我们想要的效果:
if(!Function.prototype.softBind){
Function.prototype.softBind = function(obj){
var fn = this;
//捕获所有curried参数
var curride = [].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;
};
}
除了软绑定之外,softBind(..)
的其他原理和ES5内置的bind(..)
类似。它会对指定的函数进行封装,首先检查调用时的this
,如果this
绑定到全局对象或者undefined
,那就把指定的默认对象obj
绑定到this
,否则不会修改this
。此外,这段代码还支持可选的柯里化
this词法
一、箭头函数不使用this
的四种标准规则,而是根据外层(函数或者全局)作用域来决定this
。
二、箭头函数虽然没有prototype
,但是instanceof Function
为true
三、箭头函数在执行时进行动态编译,并注入代码捕获当前环境中的this
,并实现硬绑定