深入了解this机制

65 阅读12分钟

this

关于this

为什么用this

使用this可以隐式传递一个对象引用,因此可以将API设计的更加简洁并易于复用

 //使用this
 function identify(){
     return this.name.toUpperCase();
 }
 function speak(){
     var greeting = "Hello,I'm a" + identify.call(this);
     console.log(greeting);
 }
 var me = {
     name: "people";
 }
 var you = {
     name: 'pig';
 }
 identify.call(me);  //PEOPLE
 identify.call(you); //PIG
 speak.call(me);     //Hello,I'm a PEOPLE;
 speak.call(you);        //Hello,I'm a PIG;
 ​
 //不使用this,就只能通过显示传入一个上下文对象
 function identify(context){
     return context.name.toUpperCase();
 }
 function speak(context){
     var greeting = "Hello,I'm a" + identify(context);
 }

this到底是啥

this是在运行时进行绑定的,并不是在编写时绑定,他的上下文取决于函数调用时的各种条件

this的绑定和函数声明位置没有任何关系只取决于函数的调用方法

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文),这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,this就是这个记录的一个属性,会在函数执行过程中用到

对this的误解

指向自身

这里我们来看一个例子:

 function foo(num){
     console.log("foo:"+num);
     this.count++;   //this不是指向那个函数对象
 }
 foo.count = 0;  //向函数对象添加了一个属性count
 var i;
 for(i=0; i<10; i++){
     if(i>5){
         foo(i);
     }
 }
 console.log(foo.count);
 //foo: 6
 //foo: 7
 //foo: 8
 //foo: 9
 //0

为什么上述的代码片段中,this行为与预期不一样?

很明显,这里的this并不是指向foo本身,其实this指向的是全局window

如果要从函数对象内部引用它自身,那只使用this是不够的,一般来说需要通过一个指向函数对象的词法标识符来引用他

对比:

 function foo(){
     foo.count = 4;  //具名函数可以指向自身
 }
 setTimeout(function(){
     //匿名函数无法指向自身
 },10);

注:有一种方法可以让匿名函数指向自身arguments.callee,但是这种方法现在已经弃用了,因为访问arguments是一个昂贵的操作,他是一个很大的对象,每次递归调用都需要重新创建,影响浏览器性能,还会影响闭包

解决上述问题的方法:

  1. 使用词法作用域

  2. 强制this指向foo函数对象

     function foo(num){
         console.log("foo:"+num);
         this.count++;
     }
     foo.count = 0;
     var i;
     for(i=0; i<10; i++){
         if(i>5){
             foo.call(foo, i);   //确保this指向函数对象foo本身
         }
     }
     console.log(foo.count);
     //输出预期结果
    

this的作用域

this在任何情况下都不指向函数的词法作用域

作用域无法通过JS代码访问,它存在于JS引擎内部

 function foo(){
     var a = 2;
     this.bar();
 }
 function bar(){
     console.log(this.a);
 }
 foo();  // undefined

解析:

  • 这段代码试图使用this.bar()去调用bar(),一般直接调用bar()
  • 试图使用this连接foobar的词法作用域

绑定规则

函数在执行过程中是如何决定this的绑定对象的呢?

默认绑定

最常用的函数调用类型:独立函数调用

这个规则可以看成是无法应用其他规则时的默认规则

 function foo(){
     console.log(this.a);
 }
 var a = 2;
 foo();  // 2,函数调用时应用了this的默认绑定,所以指向全局对象

foo直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法使用其他规则

注意:

  • 严格模式环境下,不能将全局对象用于默认绑定,所以**this会绑定到undefined**

     function fn(){
         'use strict';
         console.log(this)   //undefined
         console.log(this.a);
     }
     var a = 2;
     fn()    //报错
    
  • 函数以及调用都暴露在严格模式时this会绑定到undefined

     'use strict';
     var a = 2;
     function fn(){
         console.log(this)   //undefined
         console.log(this.a);    //报错
     }
     fn();
    
  • 严格模式下调用不在严格模式中的函数不会影响this指向

    var a = 2;
    function fn(){
        console.log(this);	//window
        console.log(this.a);	//2
    }
    (function(){
        'use strict';
        fn();
    })();
    

隐式绑定

判断调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含

  • 函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,也就是说,如果函数调用时,前面存在调用它的对象,那么this就会绑定到这个对象上

    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: 20,
        foo: foo
    }
    var obj1 = {
        a: 40,
        obj2: obj2
    }
    obj1.obj2.foo();	// 20
    

    注:但是如果obj2没有a,那么最终就会输出undefined,作用域链跟原型链并不一样

隐式丢失

this绑定有个问题:被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定

function foo(){
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
var bar = obj.foo;	//函数别名,是obj.foo的一个引用
//但是,实际上他引用的是foo本身,所以此时bar是一个不带任何修饰的函数调用,所以也就应用了默认绑定
var a = 'global';	//全局对象上的属性
bar(); 	//'global'

还有一种情况(传入回调函数):

function foo(){
    console.log(this.a);
}
function doFoo(fn){		//函数传递实际上是一种隐式赋值
    fn();	//所以此处的fn实际上是foo本身
}
var obj = {
    a: 2,
    foo: foo
}
var a = 'global';
doFoo(obj.foo);		//'global'

并且,把函数传入内置函数而不是自己声明的函数,也会出现回调函数丢失this绑定的情况

注:关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this

如果嵌套函数作为方法调用,其this的值指向调用它的对象

如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)

注:并不是所有的隐式绑定丢失都指向全局对象

var a = 2;
let obj = {
    a: 3,
    fn: function(){
        console.log(this.a);
    }
}
let obj2 = {
    a: 4
}
obj2.fn = obj.fn;	//此处虽然丢失了obj的隐式绑定,但是在赋值过程中,又建立了新的隐式绑定,指向obj2
obj2.fn();	// 4

显式绑定

在隐式绑定中,我们必须在一个对象的内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上

但是现在想要不在对象内部包含函数引用,而在某个对象上强制调用函数,怎么做?

JS提供了两个函数:call()apply()

第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this

因为可以直接指定this的绑定对象,因此称为显式绑定

function foo(){
    console.log(this.a);
}
var obj = {
    a: 2
}
foo.call(obj);	// 2,此处将foo的this强制绑定到obj上了

但是如果使用call()apply()的时候,传入一个原始值(字符串,布尔值,数字)来当作this的绑定对象,这个原始值会被转换成它的对象形式new String()new Boolean()new Number()),这就被称为装箱

如果call()apply()指向null或者undefined,那么this将指向全局对象

但是显示绑定并不能解决丢失绑定的问题

硬绑定

function foo(){
    console.log(this.a);
}
var obj = {
    a: 2
}
var bar = function(){
    foo.call(obj);	//在bar内部手动调用了foo.call(obj),强制把foo的this绑定到obj
    //所以无论如何调用函数bar,都总会手动在obj上调用foo
}
bar();	//2
setTimeout(bar, 100);	// 2
bar.call(window);	//2

应用场景

  • 创建一个包裹函数,负责接收参数并返回值

    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
    
  • 创建一个可以重复使用的辅助函数

    funciton foo(something){
        console.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 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);	//会返回一个新函数,会将指定的参数设置为this的上下文并调用原始函数
var b = bar(3);		//2 3
console.log(b);		//5

API调用的上下文

第三方库许多函数,以及JS语言和宿主环境中有许多新的内置函数,都提供了一个可选的参数,一般称为上下文(context),作用和bind一样

function foo(el){
    console.log(el, this.id);
}
var obj = {
    id: 'aaa'
}
[1, 2, 3].forEach(foo, obj);	//1 aaa 2 aaa 3 aaa

注:call、apply与bind有什么区别

  • call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数
  • bind属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑
  • call与apply功能完全相同,唯一不同的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多一步解析数组

new绑定

JS中的构造函数,在JS中,构造函数只是一些使用new操作符时被调用的函数,并不会属于某个类,也不会实例化一个类

实际上他们并不能说是一种特殊类型的函数,他们只是被new操作符调用的普通函数而已

包括内置对象函数在内的所有函数都可以用new进行调用,这种函数调用被称为构造函数调用,但是有一个区别:实际上并不存在所谓的构造函数,只有对于函数的构造调用

使用new调用函数:

  1. 创建一个全新的对象
  2. 这个新对象会被执行[[Prototype]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
    this.a = a;
}	
var bar = new foo(2);	//会构造一个新对象并把它绑定到foo()调用中的this上
console.log(bar.a);		// 2

this绑定优先级

默认绑定的优先级最低

隐式绑定和显式绑定

显示绑定优先级要大于隐式绑定

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绑定

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);
console.log(obj1.a);	//2
console.log(bar.a);		//4

new绑定和显示绑定

new绑定优先级要大于显示绑定

function foo(something){
    this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);	//bar被硬绑到obj1
bar(2);
console.log(obj1.a);	// 2

var baz = new bar(3);
console.log(obj1.a);	// 2,此处没有把obj1.a改成3
console.log(baz.a);		// 3,而是修改了baz新对象

结论:

  1. 函数是否在有new中调用,是的话this绑定的是新创建的对象
  2. 函数是否通过callapply显示绑定,是的话this绑定的是指定的对象
  3. 函数是否在上下文对象中调用,是的话绑定那个上下文对象
  4. 如果都不是就使用默认绑定,严格模式下绑定到undefined,否则绑定到全局对象

绑定例外

被忽略的this

如果nullundefined作为this的绑定对象传入callapplybind这些值在调用时会被忽略,实际上应用的是默认绑定规则

但是什么情况下会传入null呢? 看看apply的应用场景:

apply可以来展开一个数组,并当作参数传入一个函数

function foo(a, b){
    console.log("a:" + a + ", b:" + b )
}
foo.apply(null, [2, 3]);  //a:2, b:3

此处null作为一个占位符,因为不关心this的指向

这样可能会产生一些不可预计的后果,所以现在有另外一种方案让this更安全

创建一个DMZ(非军事区)对象,作为一个空的非委托的对象

任何的this使用都可以限制在这个空对象中,不会对全局对象产生任何影响

创建方法:Object.create(null) ,这种方式和直接{}很像,但是它不会创建Object.prototype,比{}更空

function foo(a, b){
    console.log("a:" + a + ", b:" + b )
}
var DMZ = Object.create(null);
foo.apply(DMZ, [2, 3]);  //a:2, b:3

间接引用

间接引用最容易发生在赋值的时候,应用默认绑定

下面的赋值表达式p.foo = o.foo的返回值是目标函数的引用

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,调用的是foo本身,所以应用默认绑定

软绑定

硬绑定可以把this强制绑定到指定的对象,防止函数调用应用默认绑定规则,但是硬绑定的灵活性很低,使用硬绑定就不能使用隐式绑定和显式绑定来修改this

软绑定让 this在默认情况下不再指向全局对象(非严格模式)或**undefined(严格模式),而是指向两者之外的一个对象(这点和硬绑定的效果相同),但是同时又保留了隐式绑定和显式绑定在之后可以修改this指向的能力**

所以就有一种绑定称为软绑定:Function.prototype.softBind

除了软绑定外,softBind其他原理和bind类似

function foo(){
    console.log("name: " + this.name);
}
var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" }
var fooOBJ = foo.softBind(obj);
fooOBJ();	//name: obj
obj2.foo = foo.softBind(obj);
obj2.foo();		//name: obj2
fooOBJ.call(obj3);	//name: obj3
setTimeout(obj2.foo, 10);	//name: obj

箭头函数

之前的规则可以应用于所有正常的函数,但是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

箭头函数最常用于回调函数中,可以bind一样保证函数的this被绑定到指定对象

在ES6之前,就有一种替代方案:

function foo(){
    var self = this;
    setTimeout(function(){
        console.log(self.a);
    }, 100);
}
var obj = {
    a: 2
}
foo.call(obj); 	// 2