你不知道的JS总结之—this

376 阅读8分钟

《你不知道的JS》中this总结

前言

在学习this的时候看过一些关于this这部分的文章。大部分都是零零散散的看完了就放在收藏夹里面。最近再去收藏夹里面找来看的时候发现之前那篇写的很好的this文章却不见了。这篇文章是我重新看了《你不知道的js》后对this这部分内容的总结,更多内容请参照《你不知道的javascript》。

误解

在没有看《你不知道的js》之前我也是根据他的英文翻译以为this就是指函数本身或者是函数的作用域 。但是后来学习了一系列知识之后打破我之前的一些认知。

误解一:this指向自身

   function foo(num){
        this.count++
        console.log(this)//window对象, 
        console.log(this.count) //输入了4次NaN, count 为undefined
    }
    foo.count=0;
    for(let i=0;i<10;i++){
        if(i>5){
            foo(i);
        }
    }
    console.log(foo.count)//依然为0

反逻辑】: 如果this指向自身,那么this==foo函数对象,那么this.count==foo.count==0;但是实际结果却是上面的情况。说明this不指向自身。

打印出window对象】:发现foo上确实有count属性。但是在window全局变量上面也有一个count.

误解二:指向它的作用域

第二种误解是,this指向函数的作用域。在有些情况下其实是正确的。但是实际上,this在任何情况下都不指向函数的词法作用域

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

反逻辑】如果this指向函数的作用域,那么输出的应该是2。但是输出的却是undefined。

实际上this指向

实际上this取决于调用的位置。因为他的指向是调用的时候被绑定的。 书中提到有四种this绑定规则。

默认绑定

独立函数的调用,调用函数的时候前面没有加修饰符,这种情况默认this指向window(注意如果是在严格模式下this指向undefined)。

    function foo(){
        var a = 2;
        var b =1 ;
        bar();
        console.log(this.a)//window  
    }
    var b = 23;
    function bar(){
        console.log(b,"bbbbbbbb")//23
        console.log(this) //window
    }
    foo();

逻辑】foo() 函数和bar()函数在调用时前面没有任何修饰符。尽管bar函数的调用栈在foo()函数中。但是输出的值却是23. 说明bar函数的this指向也是window.

隐式绑定

这种规则需要考虑的是调用位置是否有上下文对象。或者说是否被某个对象拥有或者包含。

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

逻辑】这种方式调用函数,隐式规则会把函数调用中的this绑定到这个函数上下文对象中。

注意】只有最后一层在调用位置中起作用。

   function foo(){
        console.log(this.a)//42
    }
    var obj2 = {
        a:42,
        foo:foo
    }
    var obj1 = {
        a:2,
        obj2: obj2
    }
    obj1.obj2.foo(); //这只有最后一层的obj2起作用

隐式调用的其他注意问题

  • 情况一: 调用函数时给调用函数一个别名会丢失隐式绑定
    function foo(){
        console.log(this.a)//全局对象
    }
    var obj = {
        a:"对象a的属性",
        foo:foo
    }
    var bar = obj.foo;//函数别名
    var a = "全局对象";
    bar();

这种情况相当于用的是默认绑定的规则。输出的是“全局对象”。

  • 情况二: 传入回调函数
   function foo(){
        console.log(this.a)//全局对象属性
    }
    function doFoo(fn){
        fn();
    }
    var obj={
        a:2,
        foo:foo
    }
    var a = "全局对象属性"
    doFoo(obj.foo)

这种情况类似于情况一。

显示绑定(call, apply)方法

在某个对象上面强制调用某个函数。

    function foo(){
        console.log(this.a)//
    }
    var obj = {
        a:2
    }
    var a = "全局"
    foo.call(obj)

注意】如果传入了一个原始值(字符串类型,boolean类型,数字类型)来当做this绑定对象,这个传入值会转化为他们的对象形式

    function foo(){
        console.log(this)
    }
    var obj = {
        a:2
    }
    var a = "全局"
    foo.call(true)
    foo.call("233")
    foo.call(1)

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

1.硬绑定

硬绑定是一种显示的强制绑定。 原理:Function.prototype.bind()会创建一个新的包装函数。这个函数会忽略它当前的this绑定。(无论绑定的对象是什么),并把我们提供的对象绑定到this上

       function foo(something) {
            console.log(this.a, something);
            return this.a + something;
        }
        var obj = {
            a: 2
        }
        var bar = function () {
            console.log(arguments)
            return foo.apply(obj, arguments)
        }
        // var b = bar(3);
        // b()
        // console.log(bar(3)) 
        bar(3)//2 3

new绑定.

在javascript中 只是被new操作符调用的普通函数。

使用new来构造函数的时候,会自动执行下面的操作。

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

优先级比较

  • 显示绑定和隐式绑定比较
       function foo(){
            console.log(this.a)
        }
        var obj1 = {
            a:2,
            foo:foo
        }

        var obj2 = {
            a:3,
            foo:foo
        }
        obj1.foo.call(obj2)//3
        obj2.foo.call(obj1)//2

obj1.foo.call(obj2) 调用方式中前半部分是隐式调用,后半部分是显示调用。但是输出的却是显示调用中的值。 说明显示调用的优先级大于隐式调用。

  • new绑定和隐式绑定比较
        function foo(something){
            console.log(this)
            this.a = something
        }
        var obj1 = {
            foo:foo
        }
        obj1.foo(4)
  function foo(something){
            console.log(this)//foo
            this.a = something
        }
   var obj1 = {
            foo:foo
    }
    var bar = new obj1.foo(4)

后面这一段代码中既有new绑定,又有隐式绑定,但是打印出来的this却是foo函数。说明new绑定优先级大于隐式绑定

  • new绑定和显示绑定
       function foo(something){
            this.a = something;
        }
        
        var obj1 = {};
        //硬绑定部分
        var bar = foo.bind(obj1);
        bar(2);
        console.log(obj1.a)//2
        //new 绑定
        var baz = new bar(3);
        console.log(obj1.a);//2
        console.log(baz.a);//3

解释】依据硬绑定的原理,bar被绑定到了obj1上,并且后面无法再更改bar的绑定。所以在硬绑定阶段输出的是2. 执行到new绑定的时候。 如果new绑定没有改变bar函数的绑定的话,输出的obj1.a应该是3,但是输出的依然是2. 说明new绑定已经改变了bar的绑定。打印出新产生的对象,发现新对象多了一个a属性。

判断this步骤

1、函数是否在new中调用?如果是的话this绑定的是新创建的对象

2、函数是否通过call、apply或者硬绑定调用,如果是的话this为传入的对象

3、函数是否在某个上下文对象中调用(隐私绑定)

4、默认绑定

绑定例外的情况

1、将null 或者undefined作为this的绑定对象传入call、apply或者bind. 此时会使用默认绑定。

 function foo(){
            console.log(this.a)
        }

  var a = 2;
  foo.call(null); //2
会传入null的情况
  • 情况一 : 展开一个数组
  • 情况二 : 函数柯里化
        function foo(a,b){
            console.log("a:"+a+",b:"+b)
        }
        //把数组展开成参数
        foo.apply(null,[2,3]);
        //使用bind进行柯里化
        var bar = foo.bind(null,2);
        bar(3)

注意】直接传入空对象可能会导致一些问题,通常的做法是传入一个特殊的空对象

       function foo(a,b){
            console.log("a:"+a+",b:"+b)
        }
        var b = Object.create(null);
        //把数组展开成参数
        foo.apply(b,[2,3]);
        //使用bind进行柯里化
        var bar = foo.bind(b,2)
        bar(3)

2、间接引用

      function foo(){
            console.log(this.a)
        }
        var a = 2;
        var o={
            a:3,
            foo:foo
        }
        var p = {a:4}
        o.foo();
        (p.foo = o.foo)()//等价于 foo()
        foo()
        console.log(o.foo)//foo函数

3、软绑定

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

1 if(!Function.prototype.softBind){
 2     Function.prototype.softBind=function(obj){
 3         var fn=this;
 4         var args=Array.prototype.slice.call(arguments,1);
 5         var bound=function(){
 6             return fn.apply(
 7                 (!this||this===(window||global))?obj:this,
 8                 args.concat.apply(args,arguments)
 9             );
10         };
11         bound.prototype=Object.create(fn.prototype);
12         return bound;
13     };
14 }
1 function foo(){
 2     console.log("name: "+this.name);
 3 }
 4 
 5 var obj1={name:"obj1"},
 6     obj2={name:"obj2"},
 7     obj3={name:"obj3"};
 8 
 9 var fooOBJ=foo.softBind(obj1);
10 fooOBJ();//"name: obj1" 在这里软绑定生效了,成功修改了this的指向,将this绑定到了obj1上
11 
12 obj2.foo=foo.softBind(obj1);
13 obj2.foo();//"name: obj2" 隐式绑定修改了foo()函数this的绑定
14 
15 fooOBJ.call(obj3);//"name: obj3" 显示绑定修改了foo()函数的this绑定
16 
17 setTimeout(obj2.foo,1000);//"name: obj1"
18 /*回调函数相当于一个隐式的传参,如果没有软绑定的话,这里将会应用默认绑定将this绑定到全局环 境上,但有软绑定,这里this还是指向obj1*/

4、箭头函数

es6中的箭头函数无法使用上面的这几种规则。因为箭头函数并不是使用function关键字定义的。而是使用=>操作符定义的。他的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

foo()函数绑定了obj1对象。bar函数是foo返回的箭头函数。如果bar是按照显示绑定规则那么输出的应该是3,但是实际却输出的为2.

最后分享大佬的文章赶快去练练手吧

LinDaiDai_霖呆呆-再来40道this面试题酸爽继续(1.2w字用手整理)