javascript中this的指向问题

 this存在于哪里?

this是一个很特别的关键字,被自动定义在所有函数的作用域中。

 

为什么要使用this,好处是什么?

能将“显式”传参转化为“隐式”传参。更简洁且易于复用。

function say1(){
            let message = "Hello,My name is " + this.name; 
            console.log(message);
        }
           var I = {name: "Mary"};
           var You = {name: "Lily"};
           say1.apply(I);    //Hello,My name is Mary
           say1.apply(You);        //Hello,My name is Lily

           //对比:
           function say2(obj){
            let message = "Hello,My name is " + obj.name; 
            console.log(message);
        }
        var I = {name: "Mary"};
           var You = {name: "Lily"};
           say2(I);        //Hello,My name is Mary
           say2(You);        //Hello,My name is Lily

这么好。该如何使用,又有什么难点呢?

 

我们对this有什么误解呢?

 1、认为this指向函数自身

foo.count = 0;
var count = 0;
function foo(num){
console.log("foo: "+num);
console.log(this);
this.count++;
}

for(let i=0;i<5;i++){
if(i>2){
foo(i); //foo: 3 foo: 4
}
}
//与我们设想的结果不一样,显然,this并不是指向函数自身
console.log(foo.count); //0
console.log(count); // 2  (增加数量的是window下的count)

        //解决方法1:(回避this问题)
        bar.count = 0;
        function bar(num){
            console.log("bar: "+num);
            bar.count++;
        }

        for(let i=0;i<5;i++){
            if(i>2){
                bar(i);        //bar: 3      bar: 4
            }
        }
        console.log(bar.count);        //2

        //解决方法2:修改this的指向
        baz.count = 0;
        function baz(num){
            console.log("baz: "+num);
            this.count++;
        }
        for(let i=0;i<5;i++){
            if(i>2){
                baz.call(baz,i);        //baz: 3      baz: 4
            }
        }
        console.log(baz.count);        //2

  2、认为this指向该函数的作用域。

var a = 454;
function foo(){
var a = 2;
bar();
}

function bar(){
console.log(this.a);
}
//bar() 中的this 是无法访问到foo() 词法作用域中的变量a的
foo(); //454 (访问到的是最外层window下的变量a)

那么,

this的指向是什么呢?

this 的原理类似于我们之前在  作用域  里面提到的动态作用域,是在运行时才进行绑定的,也就是说它的指向完全取决于函数在哪里被调用。

//当前 调用位置 是 当前调用栈 的上一级
        function foo(){
            //当前调用栈是:foo ; 当前调用位置是:全局作用域
            console.log('foo');
            bar();
        }
        function bar(){
            //当前调用栈是:foo>bar ; 当前调用位置是:foo
            console.log('bar');
            baz();
        }
        function baz(){
            //当前调用栈是:foo>bar>baz ; 当前调用位置是:foo>bar
            console.log('baz');
        }
        
        foo();    

 

this的绑定规则是什么呢?有没有什么规律

有四种,分别是 : 默认规则、隐式绑定、显示绑定、new绑定。

1、默认绑定:直接使用不带任何修饰的函数引用进行调用,才使用默认绑定,指向全局对象。(注意:严格模式下,不能使用

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

        //严格模式下不能使用默认模式
        function bar(){
            "use strict";
            console.log(this.a);
        }
        var a = 2;

        bar();        //Cannot read property 'a' of undefined 

2、隐式绑定:

对象属性引用链:最后一层控制this上下文。常出现的问题(通常出现在调用函数别名和调用回调函数)是隐式丢失,使用了默认规则

function foo(){
            console.log(this.a);
        }
        const obj1 = {
            a: 1,
            foo: foo
        };
        const obj2 = {
            a: 2,
            foo: obj1.foo,
            bar:obj1
        };

        var a = 3;
        obj2.foo();        //2
        obj2.bar.foo();        //1

        var baz = obj1.foo;
        //隐式丢失(调用函数别名),使用默认规则,这时的this指向全局对象
        baz();    //3
        //对比:此时使用的隐式规则
        obj1.foo();        //1

        function doFun(fn){
            fn();
        }
        //隐式丢失(调用回调函数),使用默认规则,这时的this指向全局对象
        doFun(obj1.foo);    //3

3、显式绑定

与原型有关,可以通过call() 和 apply() 来修改this的指向

function foo(b,c){
            console.log(this.a);
            console.log(b,c);
        }

        var a = 0;
        foo(2,3);        //0     2 3   
        var obj = {
            a:'obj',
            b:'foo-a',
            c:'foo-b'
        };
        //call() 和 apply() 只有参数格式不同的区别
        foo.call(obj,obj.b,obj.c);    //obj     foo-a foo-b
        foo.apply(obj,[obj.b,obj.c]);    //obj     foo-a foo-b

call() 的实现原理:

Function.prototype.myCall = function(context,...args){
            let cxt = context || window;
            //将当前被调用的方法定义在cxt.func上.(为了能以对象调用形式绑定this)
            //新建一个唯一的Symbol变量避免重复
            let func = Symbol();
            console.dir(this);  //getName()  ---> 这里是隐式绑定,
            cxt[func] = this;
            args = args ? args : []
            //以对象调用形式调用func,此时this指向cxt 也就是传入的需要绑定的this指向
            const res = args.length > 0 ? cxt[func](...args) : cxt[func]();
            //删除该方法,不然会对传入对象造成污染(添加该方法)
            delete cxt[func];
            return res;
        }

        let obj = {
            name: "obj",
            getName(){
                console.log(this.name);
            }
        };
        let obj1 = {
            name: "obj1"
        };

        obj.getName.myCall(obj1);  // obj1

 

3.1 显式绑定的变种(硬绑定)能解决前面的隐式丢失的问题

var a = 1;
        function foo(b=''){
            console.log(this.a,b);
        }
        var obj1 = {
            a: 2,
            foo:foo
        };
        var obj2 = {
            a: 3,
            b:obj1.foo,
            c:obj1
        };
        
        //隐式规则
        obj2.b();    //3
        obj2.c.foo();    //2
        //默认规则
        foo();        //1

        //隐式丢失的两种情况: 函数别名 和 回调函数
        var bar = obj2.b;
        bar();   //1

        setTimeout(obj2.c.foo,100);        //1

        //解决方法:硬绑定
        var bar1 = function(){
            obj2.b.call(obj2,arguments[0]);
        }
        bar1('bar1');        //3 "bar1"

        var bar2 = function(){
            obj2.c.foo.call(obj1,arguments[0]);
        }
        bar2('bar2');        //2 "bar2"

4、new 绑定

使用new操作符调用的函数被我们称之为  “构造函数 ”。它与普通函数并没有什么区别,只是被new操作符调用而已。发生构造函数调用时,会自动执行下面的操作:

  (1)创建(或者说构造)一个全新的对象。

  (2)这个新对象会被执行【原型】连接。

  (3)这个新对象会绑定到函数调用的this。

  (4)如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

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

 

上面这四种规则的优先级是什么呢?

new绑定 > 显示规则 > 隐式规则 > 默认规则

function foo(){
            console.log(this.a);
        }
        var obj1 = {
            a: 1,
            foo: foo,
            baz:baz
        };
        var a = 2;
        //隐式
        obj1.foo();        //1
        //显示
        obj1.foo.call(window);        //2
        //结论:显式 > 隐式

        function baz(num){
            this.a = num;
        }
        obj1.baz('隐式规则');
        console.log(obj1.a);        //隐式规则
        var bar = new obj1.baz("new模式");
        console.log(obj1.a);        //隐式规则
        console.log(bar.a);        //new模式
        //结论: new模式 > 隐式
        
        console.log("..........");
        var fn = baz.bind(window);
        fn(3);
        console.log(a);        //3
        var obj = new fn(100);
        //等价于
        var obj1 = new (baz.bind(window))(100);
        console.log(obj1.a);    //100
        console.log(obj.a);        //100
        //结论: new模式 > 显式

 

规则以外的意外

万事总有例外,并不总是遵循上面 的规则。如一下情况:

1、被忽略的this

  在调用call 或 apply 时, 第一参数传入了null 或 undefined 来进行this 的绑定,此时使用的是默认规则。 一般这么做,只是占位而已,函数中都不会调用到this,但不排除某些函数用到了this。所以为了安全起见,还是传入一个空的对象更好。

//通常的情况
        function foo(a,b){
            console.log(`${a}...........${b}`);
        }
        foo.call(null,3,4);        //3...........4

        //最好使用
        var NONE = Object.create(null);
        foo.call(NONE,3,4);        //3...........4

2、es6中的箭头函数

  不适用上面的四个规则,是根据外层的作用域来确定 this 的,用更常见的词法作用域来代替传统的this机制。

var a = 1;
        function fun(fn){
            fn();
        }
        function foo(){
            fun(()=> {
                console.log(this.a);
            });
        }
        var obj = {
            a: 2
        };
        //这里的箭头函数跟外层的 foo() 指向一致
        foo();        //1
        foo.call(obj);        //2

        //等价于(定义变量替换this,绕过this机制)
        function foo1(){
            var self = this;
            fun(function (){
                console.log(self.a);
            });
        }
        //等价于(使用bind()修改this指向)
        function foo2(){
            fun(function (){
                console.log(this.a);
            }.bind(foo2));
        }
        //效果跟上面的箭头函数是一样的
        foo1();        //1
        foo1.call(obj);        //2
        foo2();        //1
        foo2.call(obj);        //2

3、隐式丢失

  在前文介绍隐式规则的时候已经有提到了,这里就不再多说。

 

总结

  this的内容大概就这么多。重点是看函数运行时来确定this 的绑定,绑定的规则有:new绑定(使用new关键字生成的实例) > 显式规则 (通过call() 、apply()、bind()等方法来修改this的指向)> 隐式规则(通过对象属性的引用,由最后一层控制this的指向) > 默认规则(其他规则都不适用时,使用默认规则,一直都指向全局对象)。不过有些this的绑定并不遵守上面的规则,例如:ES6中的箭头函数、使用函数别名和回调函数后出现的隐式丢失等情况。

posted @ 2021-03-04 14:47  拉布拉多~  阅读(121)  评论(0编辑  收藏  举报