调用的位置:
函数中this的绑定取决于它所调用的位置。
通常来说,寻找调用位置就是寻找"函数被调用的位置",最重要的是要分析调用栈(就是为了当前执行位置所调用的所有函数)。
通过例子来看调用栈和调用位置:
function a(){ //当前调用栈是:a //因此当前调用位置是全局作用域 console.log('this is a'); b(); //b的调用位置 } functoin b(){ //当前调用栈:a--->b console.log('this is b'); c(); //c的调用位置 }; function c(){ //当前调用栈:a--->b--->c console.log('this is c'); }; a(); //--->a的调用位置
浏览器中一般都带有查看当前函数调用栈的调试器,以下是在Google中在控制点上的查看调用栈方式:
绑定规则:
this的绑定规则通常有4种:
1,默认绑定:最常用的函数调用类型,独立函数调用。可以把这条规则看作是无法应用其他规则的默认规则。
function bar(){ console.log(this.a); } var a = 111; bar(); //111
函数调用时应用的是this的默认绑定,因此this指向的是全局对象。
如何知道bar中的this绑定是使用的默认绑定规则?在代码中,bar()是直接使用不带任何修饰的函数引用进行调用的,因此
,只能使用默认绑定规则,而不能使用其他绑定规则。
如果使用严格模式"use strict",那么全局对象将无法使用默认绑定,这个时候this指向undefined:
function bar(){ 'use strict'; console.log(this.a); } var a = 111; bar(); //TypeError: Cannot read property 'a' of undefined
2,隐式绑定:隐式绑定要考虑的规则是调用位置是否有上下文对象。或者说是否被某个对象拥有或包含。
function foo(){ console.log(this.a); } var a = '111'; var obj = { a:'222', foo:foo }; obj.foo(); //'222'
上面的例子中,obj对象的foo属性引用了foo方法,函数foo严格来说不属于obj对象。然而,调用位置会使用obj上下文来引
用函数,因此可以说函数被调用时obj拥有或者包含它。当函数引用有上下文对象时,隐式绑定规则会将函数的this绑定到
这个上下文对象中。
隐藏丢失(一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会引用默认绑定):
function bar(){ console.log(this.a); } var obj = { a:111, foo:bar }; var a = '*****'; var baz = obj.foo; baz(); // '*****'
虽然baz是obj.foo的一个引用,但实际上,它引用的是bar函数本身。因此,此时的bar是一个不带任何修饰的函数
调用,因此使用默认绑定。
隐藏丢失一种更微妙和常见的情况发生在传入回调函数时:
function bar(){ console.log(a); } var obj = { a:111, foo:bar }; function baz(fn){ //隐式的--->var fn = bar fn(); } var a = '*****'; baz(obj.foo); // '*****'
将函数传入内置函数会怎么样呢?
function bar(){ console.log(this.a); } var obj = { a:111, foo:bar }; var a = '*****'; window.setTimeout(obj.foo,0); //'*****'
为什么会这样呢?其实JavaScript内置的setTimeout的实现和下面的代码类似:
function setTimeout(fn,delay){
//隐式的--> var fn = bar;
}
3,显示绑定:使用call与apply可以显示的绑定this到指定参数对象。
看个例子:
function bar(){ console.log(this.a); } var obj = { a:111 }; bar.call(obj); //111
注:当你传入一个原始值(字符串,数字,布尔类型)作为this指向的时候,这个原始值会被转为它的对象形
式(new Number,new String,new Boolean),这通常被称为'装箱'。
硬绑定的典型应用场景就是创建一个包裹函数,传入参数并返回接收到的值:
function bar(argum){ console.log(this.a,argun); return this.a + argum; } var obj = { a:111, foo:bar }; function baz(){ return bar.apply(obj,arguments); }; baz(222); //333
另一个方法是创建一个可以重复使用的方法:
function bar(argum){ console.log(this.a,argum); return this.a + argum; } var obj = { a:111, foo:bar }; function bind(fn,obj){ return function(){ return fn.apply(obj,arguments); } } var b = bind(bar,obj); console.log(b(222)); //111,222,333
由于硬绑定是非常常见的一种使用场景,所以ES5中提供了内置的方法,Function.prototype.bind,用法如下:
function bar(argum){ console.log(this.a,argum); return this.a + argum; } var obj = { a:111, foo:bar }; var b = bar.bind(obj); console.log(b(111));
bind会返回一个硬编码的新函数,它会把参数设置为this的上下文对象,并调用原始函数。
在许多第三方库与JavaScript语言和宿主环境许多新的内置函数,都提供了一个可选参数,通常被称
为‘上下文’,其作用和bind一样,确保你的回调函数使用指定的this。例:
function bar(val){ console.log(val,this.name); }; var obj = { name:'lebron' }; [1,2,3].forEach(bar,obj); // 1,'lebron' // 2,'lebron' // 2,'lebron'
4,new绑定:
JavaScript中的构造函数:JavaScript中的构造函数只是一些使用new操作符时被调用的函数,它并不会属于某个
类,也不会实例化一个类,实际上,它都不算是一种特殊的函数类型,它只是被new
操作符调用的普通函数而已。实际上并不存在所谓的‘构造函数’,只有对函数的构造调用。
发生构造函数调用时,会有以下操作:1,创建一个全新的对象。
2,这个对象会被执行[[原型]]链接。
3,这个新对象会绑定到函数调用的this。
4,如果函数没有其他返回对象,则返回这个新对象。
使用new调用函数的时候,我们会构建一个新对象,并把它绑定到new调用函数的this上。
优先级:
对比下显示绑定和隐式绑定哪个优先级更高一些:
function bar(){ console.log(this.a); } var obj1 = { a:111, foo:bar //隐式绑定 }; var obj2 = { a:222, foo:bar //隐式绑定 }; //调用隐式绑定的bar obj1.foo(); //111 obj2.foo(); //222 //调用显式绑定的bar obj1.foo.call(obj2); //222 obj2.foo.call(obj1); //111 //结论:显式绑点优先级高于隐式绑定
将obj1中隐式绑定的bar显示绑定到obj2,结果打印的是obj2中的a ,所以显然显式绑定>隐式绑定。
再对比一下new绑定与显式绑定的优先级:
function bar(argum){ this.a = argum; }; var obj1 = { foo:bar }; var obj2 = {}; obj1.foo(2); console.log(obj1.a); //2 obj1.foo.call(obj2,3); console.log(obj2.a); //3 var baz = new obj1.foo(4); console.log(obj1.a); //2 console.log(baz.a); //4 // 结论:new绑定优先级>隐式绑定
可以看到new绑定的优先级高于隐式绑定。
最后:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
判断this:
我们可以根据优先级来判断函数在某种调用位置使用的是哪条规则:
1,函数是否在new中调用,如果是的话this绑定的是新创建的那个对象。
2,函数是否通过call,apply(显示绑定)或硬绑定,如果是则指向的是指定对象。
3,函数是否在某个上下文对象中调用,这执行这个上下文对象。
4,如果都不是则使用默认绑定。
绑定例外:
如果将null,undefined作为上下文对象参数传递给call,apply,bind的时候,这些值在调用时会被忽略掉,实际应用的是默认绑定规则:
function bar(){ console.log(this.a); } var a = 111; //使用undefined和null作为上下文对象 bar.call(undefined | null); //111
硬绑定这种方式可以把this绑定到指定的对象,防止函数调用使用默认绑定规则。但是问题在于,硬绑定会大大降低函数的灵活性,使
用硬绑定之后就无法使用隐式绑定或显式绑定修改this绑定。
可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留显式绑定与隐式绑定修改this
的能力,可以通过一种称为软绑定的方式来实现:
if(!Function.prototype.softBind){ Function.prototype.softBind = function(obj){ var fn = this; //第一个参数为默认绑定的上下文对象,其他参数均为参数传递 var currId = [].slice.apply(arguments,1); var bound = function(){ return fn.apply((!this ||(this === (window || gloable)))?obj:this,currId.concat.apply(currId,arguments)); }; bound.prototype = Object.create(fn.prototype); return bound; } }
this词法:
上面说的4种绑定规则对于所有正常函数使用,但是ES6中的箭头函数不使用上面4种规则。
function bar(){ return ()=>{ console.log(this.a); }; } var obj1 = {a:111}; var obj2 = {a:222}; var baz = bar.call(obj1); baz(); //111 baz.call(obj2); //111
bar中的箭头函数会捕获bar调用时的this,由于bar的this绑定到obj1,所以baz的this也是绑定
到obj1,箭头函数的绑定无法被修改,所以再次call到obj2时失效。