上篇记录了对于this的一些误解,并且明白了每个函数的this是在调用时被绑定的,完全取决于函数的调用位置。
调用位置
在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用位置才能明白this到底引用的是什么。
通常来说,寻找调用位置就是寻找‘函数被调用的位置’,但是做起来并没有那么简单,因为某些编程模式可能会隐藏真正的调用位置。最重要的是分析调用栈(就是为了到达当前执行位置所调用的所有函数),我们关心的调用位置就在当前正在执行的函数的前一个调用中。
function baz(){
//当前调用栈:baz,因此当前调用位置是全局作用域
console.log('baz');
bar();//bar的调用位置
}
function bar(){
//当前调用栈是baz->bar,因此当前的调用位置在baz中
console.log('bar');
foo();//foo的调用位置
}
function foo(){
//当前调用栈是baz->bar->foo,因此当前调用位置在bar中
console.log('foo');
}
baz();//baz的调用位置
绑定规则
接下来我们看看在函数的执行过程中调用位置如何决定this的绑定对象
1、 默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其它规则时的默认规则
var a = 2;
function foo() {
console.log(this.a);
}
foo();//2
这里的this指向全局对象,首先,声明在全局作用域中的变量就是全局对象的一个同名属性。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。注意:如果使用严格模式,则不能将全局对象应用于默认绑定,此时this会绑定到undefined。
2、隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象
function foo(){
console.log(this.a);
}
var obj={
a:2,
foo:foo
}
obj.foo();//2
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。举例来说:
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="oops,global"
bar();//"oops,global"
此处bar是obj.foo的一个引用,但实际上它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时,
function foo(){
console.log(this.a);
}
function dofoo(fn){
fn();
}
var obj={
a:2,
foo:foo
}
var a="oops,global";
doFoo(obj.foo)
书上说:参数传递其实就是一种隐式赋值,因此我们传入函数时同样会被隐式赋值,所以结果和上面的例子一样,在函数里面调用时同样引用的是foo函数本身,因此应用了默认绑定。
就像我们看到的那样,回调函数丢失this绑定是非常常见的。除此之外,还有一种情况this的行为会出乎我们的意料,调用回调函数的函数可能会修改this,在以前一些流行的js库中事件处理器会把回调函数中的this强制绑定到触发事件的DOM元素上,这在一些情况下可能很有用,但是有时可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。无论是哪种情况,this的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没有办法控制调用位置以得到期望的绑定。后面我们会介绍如何通过固定this来修复这个问题。
3、显式绑定
如同上面的笔记,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。那么,如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用该函数,此时我们可以call和apply来进行显式绑定。它们的第一个参数是一个对象,在调用函数时将其绑定到this。
function foo(){
console.log(this.a);
}
var obj={
a:2
};
foo.call(obj);//2
通过foo.call(...),我们可以在函数调用foo时强制把它的this绑定到obj上。如果这里传入了一个原始值(字符串类型、布尔类型、数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象方式(也就是new String(...)、new Boolean(...)、new Number(...),这通常被称作“装箱”)。call和apply的区别在于apply的后续参数是参数数组,call的后续参数是参数列表。
然而,显式绑定仍然无法解决我们之前提出的丢失绑定问题。
(1)硬绑定
硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值:
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 function(){
return fn.apply(obj,arguments)
}
}
var obj1={
a:2
}
var bar=bind(foo,obj1)
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的上下文。
(2)API调用的“上下文”
第三方库中的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(...)一样,确保将回调函数中的this指向传入的上下文参数
function foo(el){
console.log(e,this.id);
}
var obj={
id:"awesome"
}
[1,2,3].forEach(foo,obj);//这里调用foo时把this绑定到obj
这些函数实际上就是通过call(...)或者apply(...)实现了显示绑定,这样可以少写一些代码。
4、new绑定
在讲解最后一条this绑定规则之前,首先需要了解一个非常常见的关于JavaScript中函数和对象的误解。在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数,进行实例化,通常的形式是var something=new MyClass(...)
.但是js中实际上没有类,也就没有实例化这个概念,js中一切都是对象,但是js中也有一个new操作符,使用方法看起来和那些面向类的语言一样,然而,js中new的机制实际上和面向类的语言完全不同。首先我们重新定义一下js中的"构造函数",在JavaScript中,构造函数只是一些使用new操作符时被调用的函数,它们并不属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。举例来说,当Number(...)作为构造函数被调用时,ES5.1中这样描述:
当Number在new表达式中被调用时,它是一个构造函数,它会初始化新创建的对象
所以,包括内置对象函数(比如Number(...)在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别,实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
在使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
(1)创建或者说构造一个全新的对象
(2)这个新对象会被执行[[Prototype]]连接
(3)这个新对象会绑定到函数调用的this
(4)如果函数没有返回其它对象,那么new表达式中的函数调用会自动返回这个新对象
使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo(...)调用中的this上。new是最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。
网友评论