第2章:this全面解析
2.1 调用位置
在理解this的绑定之前,首先要理解“函数调用的位置”,即函数在代码中被调用的位置。只有仔细分析了调用位置才能解释,函数中的this到底引用的是什么?
寻找“函数被调用的位置”,其实并没有想象中的简单,因为JS是很灵活的语言,经常将函数也作为参数进行传递,可能会隐藏真正的调用位置。
所以我们需要分析函数的调用栈。所谓调用栈,就是为了到达当前执行位置,所调用过的所有函数,所以我们可以把调用栈想象成一个函数调用链,举例说明:
function baz(){ //当前位置是baz console.log("my name is baz"); //baz中调用bar bar(); }function bar(){ //当前位置是bar console.log("my name is bar"); //bar中调用foo foo(); }function foo(){ //当前位置是foo console.log("my name is foo"); }//window下调用bazbaz();/* * 所以foo的调用栈(链)就是: * window -> baz -> bar -> foo * /
2.2 绑定规则
通过函数的调用位置,并应用JavaScript中四条决定this绑定的规则,就能分析this的引用值了。
2.2.1 默认绑定
首先是最常用的函数调用类型:独立函数调用。所谓独立函数调用,就是没有应用其他规则的默认调用规则。举例:
var a = 2;function foo(){ console.log(this.a); } foo(); //输出 2
当调用
foo()
函数时,this.a
指向了全局变量a
,因为在默认绑定下,this
指向全局对象。那如何辨别这里应用的是默认绑定呢?这时候,就需要运用我们前面讲的“分析函数调用位置”了,在这段代码中,
foo()
函数是直接调用的,不带任何修饰,也不被任何函数包含,所以可以确定是默认绑定。
注意:如果 函数内使用严格模式(strict mode) ,是不能将全局对象用于默认绑定的,最终
this
会绑定到undefined
上。举例说明:var a = 2;function foo(){ "use strict"; console.log(this.a); } foo(); //输出 TypeError : this is undefined但 在严格模式下调用函数 ,则不影响默认绑定。举例说明:
var a = 2;function foo(){ console.log(this.a); } (function(){ "use strict"; foo(): // 输出2)();由于我们可能会使用众多第三方库,所以代码中可能会混合使用strict模式和非strict模式,因此一定要注意这类的兼容性问题。
2.2.2 隐式绑定
第二条规则,就是通过函数调用位置,函数是否属于某个对象的属性。
var obj = { a : 2, foo : foo }function foo(){ console.log(this.a); } obj.foo(); // 输出 2
你看
foo()
方法的声明方式,它是被当做引用属性添加到了obj
对象中,这种情况下,obj
对象拥有/包含了foo()
方法。当函数有包含自己的对象时,隐式绑定规则会把
this
绑定到这个对象。因此,调用
foo()
时,this
被绑定到了obj
对象,在函数中this.a
和obj.a
的引用是一样的。值得注意的是,如果是多层嵌套对象下的函数,就只在最后一层中起作用。举例:
function foo(){ console.log(this.a); }var obj1 = { a : 2, obj2 : { a : 42, foo : foo } } obj1.obj2.foo(); //输出 42
值得注意的是,如果将
obj1.obj2.foo
函数的引用赋值给另一个变量,然后以默认绑定的方式调用函数,不管是自定义的函数,还是JS的内置函数,则还是会应用默认绑定规则:
var a = 'oops,global';var bar = obj1.obj2.foo;function runFoo(){ obj1.obj2.foo(); }// 都是输出 'oops,global'bar(); runfoo(); setTimeout(obj1.obj2.foo,100);
2.2.3 显示绑定
如果不想在对象内部包含函数引用,想在某个对象上强制调用函数,该怎么做呢?
JavaScript中的函数都有一些特性,可以用来解决这个问题。比如函数的
call()
和applay()
方法这两个方法,传入的第一个参数是一个对象,就是留给
this
准备的,调用时会将其绑定到this
。因为可以直接指定this
的绑定对象,因此我们称之为显示绑定。
function foo(){ console.log(this.a); }var obj = { a : 2}; foo.call(obj); // 输出 2
但如果传入的参数是原始值(字符串、布尔或者数值类型)当做
this
的绑定对象的话,这个原始值会被转换成它的对象形式。也就是new String()
、new Boolean()
或者new Number()
,这个过程通常叫做装箱。1. 硬绑定
function foo(){ console.log(this.a); }var obj = { a : 2};var bar = function(){ foo.call(obj); }; bar(); // 2setTimeout(bar,100);//硬绑定不能再修改它的thisbar.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 3console.log(b); // 5
另一种方式就是创建一个可以重复使用的辅助函数:
function foo(something){ cosnole.log(this.a,something); return this.a + something; }function bind(fn,obj){ return function(){ return fn.apply(obj,arguments); } }var obj = { a : 2}var bar = bind(foo,obj);var b = bar(3); // 2 3console.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 3console.log(b); // 5
bind()
会返回一个硬编码的新函数,它会把你指定的参数设置为this
的上下文,并调用原始函数。2. API调用的“上下文”
许多函数都提供了一个可选的参数,其作用和
bind()
函数一样,确保你的回调函数使用指定的this
。举例:
function foo(el){ console.log(el,this.id); }var obj = { id : 'awesome'} [1,2,3].forEach(foo,obj);
通过
call()
和apply()
实现显示绑定,可以少写代码。
2.2.4 new绑定
在讲解最后一条
this
的绑定规则之前,首先要澄清一个常见的关于JavaScript中函数和对象的误解。在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类的构造函数,
something = new MyClass()
。然而,JavaScript中的new的机制实际上和面向类的语言完全不同。我们重新定义一些JavaScript中的“构造函数”:在JavaScript中,构造函数只是使用new操作符时被调用的函数,它们并不属于某一个对象,也不会实例化一个类。
所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上,并不存在所谓的“构造函数”,只有对函数的“构造调用”。
使用new来调用函数,会自动执行下面的操作:
创建一个全新的对象
这个新对象会被执行[[Prototype]]连接
这个新对象会绑定到函数调用的this
如果函数没有返回其他对象,那么new表达式的函数调用会自动返回这个新对象。
function foo(a){ this.a = a; }var bar = new foo(2);console.log(bar.a); // 2
使用new来调用
foo()
时,我们会构造一个新对象,把它绑定到foo()
调用中的this上,我们称之为new绑定。
2.3 优先级
上文通过大篇幅讲了函数调用中,this绑定的四条规则:默认绑定、隐式绑定、显示绑定和new绑定。但如果调用应用了多条规则就必须给这些规则设定优先级了。
毫无疑问,默认绑定的优先级是最低的,暂不考虑它。
隐式绑定和显示绑定哪一个优先级更高?我们来测试一下:
function foo(){ console.log(this.a); }var obj1 = { a : 2, foo : foo }var obj2 = { a : 3, foo : foo } obj1.foo(); //2obj2.foo(); //3obj1.foo.call(obj2); // 3obj2.foo.call(obj1); // 3
可以看到,显式绑定优先级更高。
接下来,我们要测试,new绑定和隐式绑定的优先级谁高谁低:
function foo(something){ this.a = something; }var obj1 = { foo : foo }var obj2 = {} obj1.foo(2);console.log(obj1.a); //2obj1.foo.call(obj2,3);console.log(obj2.a); //3var bar = new obj1.foo(4);console.log(obj1.a); //2console.log(bar.a); //2
可以看到new绑定比隐式绑定的优先级更高,
obj1.a
的值一直没改变。那new绑定和显示绑定,谁的优先级更高呢?(由于new和call/apply无法一起使用,所以通过硬绑定来测试)
function foo(something){ this.a = something; }var obj1 = {};var bar = foo.bind(obj1); bar(2);console.log(obj1.a); //2var baz = new bar(3);console.log(obj1.a); // 2console.log(baz.a); // 3
观察输出的结果,
bar
被硬绑定到了obj1
上,但new baz(3)
并没有把obj1.a
修改为3.话说回来,之所以在new中使用硬绑定函数,主要目的是想预先设置一些参数,这样在使用new进行初始化时就可以传入其他参数了。举例:
function foo(p1,p2){ this.val = p1 + p2; }var bar = foo.bind(null,"p1");var baz = new bar("p2"); baz.val; //p1p2
根据优先级就能判断函数调用时应用的是哪条规则了,判断的步骤:
函数是否进行了new调用,如果是的话,this绑定的是新创建的对象;
函数是否通过
call
、apply
或者硬绑定调用,如果有的话,this绑定的是指定的对象;函数是否在某个对象中调用,如果是的话,this绑定的是该对象;
如果都不是的话,使用默认绑定,绑定到全局对象。(严格模式下,绑定到undefined)
2.4 绑定例外
2.4.1 被忽略的this
把null或者undefined作为this传入call、apply或者bind,这些值在函数调用时会被忽略,应用默认的绑定规则:
function foo(){ console.log(this.a); }var a = 2; foo.call(null); // 2
什么场景下会传入null呢?比如使用apply来遍历输出一个数组,或者通过
bind()
进行柯里化:
function foo(a,b){ console.log('a:'+a+'b:'+b); } foo.apply(null,[2,3]); // a:2,b:3//先预先传入参数avar bar = foo.bind(null,2);//调用时再传入参数bbar(3); // a:2,b:3
但这种方式可能会导致许多难以分析和追踪的bug,我们可以用更安全的方式。
更安全的做法就是不传入null,而是传入一个空的对象,把this绑定到这个对象,就好像创建一个非军事区的隔离对象一样,以确保不会对你的程序产生任何副作用。
function foo(a,b){ console.log('a:'+a+'b:'+b); }var o = Object.create(null); foo.apply(o,[2,3]); // a:2,b:3//先预先传入参数avar bar = foo.bind(o,2);//调用时再传入参数bbar(3); // a:2,b:3
我们通过
Object.create(null)
来创建对象,它和直接以字面量{}
创建对象很相似,但前者不会创建Object.prototype
的委托,所以它比{}
更空。
2.4.2 间接引用
另一个需要注意的是,可能会有意无意的创建一个函数的“间接引用”,而在这种情况下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生:
function foo(){ console.log(this.a); }var a = 2;var o = { a : 3, foo : foo }var p = { a : 4} o.foo(); // 3(function(){ p.foo = o.foo })(); // 2
2.4.3 软绑定
硬绑定可以把this强制绑定到指定的对象,以防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,硬绑定之后,就无法使用隐式绑定或者显示绑定来修改this。
if(!Function.prototype.softbind){ Function.prototype.softbind = function(obj){ var fn = this; 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; } }function foo(){ console.log('name:' + this.name); }var obj = { name : "obj" }, obj2 = { name : "obj2" }, obj3 = { name : "obj3" };var fooOBJ = foo.softbind(obj); fooOBJ(); // name : objobj2.foo = foo.softbind(obj); obj2.foo(); // name : obj2fooOBJ.call(obj3); // name : obj3setTimeout(obj2.foo,10); // name : obj
2.5 this词法
前面解说的四条规则几乎也包含所有函数,但ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。
箭头函数使用操作符
=>
来定义,箭头函数不适用this的四种标准规则,而是根据外层作用域来决定this。
function foo(){ return (a) => { console.log(this.a); } }var obj1 = { a : 2}var obj2 = { a : 3}var bar = foo.call(obj1); bar.call(obj2); // 2 , 不是 3 !
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo(){ setTimeout(() => { console.log(this.a); },100); }var obj = { a : 2}; foo.call(obj); // 2
2.6 小结
如果要判断一个运用中函数的this绑定,需要找到函数的调用位置,然后按顺序应用四条规则来判断this的绑定对象:
由new调用?绑定到新创建的对象;
由call或者apply/bind调用?绑定到指定的对象;
由对象调用?绑定到那个对象;
默认:严格模式下绑定到undefined,否则绑定到全局对象;
有些调用无意中使用默认绑定规则。如果想“更安全” 地忽略this绑定,可以使用一个空的临时对象,比如o = Object.create(null)
,以保护全局对象。
ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this。
作者:梁同学de自言自语
链接:https://www.jianshu.com/p/0528aefd47d2