说说this

2019-05-11  本文已影响0人  Jason_Shu

  我们都知道Javascript中的「this」真的是个头痛的东西,今儿我们就来好好总结下这个「this」。

  我记得之前这块内容,我直接先背了个「口诀」。

function fn() {
  console.log(this);
}

// 1. fn(); // 指向window
// 2. fn(); // undefined(严格模式下)
// 3. a.b.c.fn() // 指向a.b.c
// 4. new fn() // 指向new的实例对象
// 5. () => { fn(); } // 箭头函数调用fn,this指向「外层代码库的this」

之前了解的不多,今天来详细解析一波。

首先我们来说说this的绑定规则。
(1)默认绑定
(2)隐式绑定
(3)显式绑定
(4)new绑定

以下全部在「浏览器环境」中。

(1)默认绑定
  在不能应用「其他绑定规则」的时候,使用「默认绑定」,通常是作为「独立函数」进行调用。

function foo() {
    console.log(this.name); // 'Jason'
}

var name = 'Jason';

foo();

  在调用「foo()」的时候,应用了「默认绑定」,this指向了全局对象window(非严格模式下),严格模式下,指向「undefined」。

(2)隐式绑定
  函数的调用是在「某个对象」上触发的,即调用位置上存在「执行上下文」。典型的形式如「xxx.fn()」。

var name = 'Jack';

function fn() {
    console.log(this.name);
}

var obj = {

    name: 'Jason',

    foo: fn

};

obj.foo(); // 'Jason';

  函数fn的声明在对象obj的外部,看起来是不属于obj对象的,但是在调用foo的时候,隐式绑定会把函数调用中的「this」(foo函数中的this)绑定到对应的「执行上下文」(此例中的obj)。注意:对象属性链只有最后一层会影响调用位置。

function fn() {
    console.log(this.name);
}

let b = {
    name: 'Jack',
    foo: fn
}

let a = {
    name: 'Jason',
    friend: b
}

a.friend.foo();

  上面代码中,foo函数的执行环境不是a,而是「a.friend」,即是「b」。

  但是隐式绑定存在一个问题:绑定丢失。我们来看下边一段代码:

function fn() {
    console.log(this.name);
}

var name = 'Jack';

let obj = {
    name: 'Jason',
    sayName: fn,
}

let say = obj.sayName;

say(); // 'Jack'

  如果我们单看「obj.sayName」,执行上下文是对象obj,但是我们把「obj.sayName」赋值给了变量say后,调用say()后,函数fn的执行上下文就变为了全局变量(window)中。
  针对这类问题,我们只要记住,形式为「xxx.fn()」才是隐式绑定,如果格式为「fn()」,前面什么都没有,那肯定不是隐式绑定,但是也不一定是「默认绑定」,下文中会解释。
  除了上述的「绑定丢失」,还有一种绑定丢失的情况,就是发生在「回调函数」中,我们再看一个例子。

function fn() {
    console.log(this.name);
}

var person1 = {
    name: 'Jason',
    sayName: function() {
        setTimeout(function() {
            console.log('Hello!', this.name);
        })
    }
};

var person2 = {
    name: 'Jack',
    sayName: fn
};


var name = 'Tom';

person1.sayName(); // 'Hello! Tom'

setTimeout(person2.sayName, 100); // 'Tom'

setTimeout(function() {
    person2.sayName();  // 'Jack'
}, 200);

我们依次说说每次输出的原因。

(3)显式绑定
  显式绑定主要是通过call,apply和bind来显式的绑定this,call,apply和bind的第一个参数就是对应的this对象,call和apply的作用一样,只是call从第二个参数开始依次传入参数,而apply是直接把所有参数集成为一个数组放到第二个参数,bind会返回一个函数,在正式执行函数的时候,优先调bind第二个后的参数。来看看代码:

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
};


var name = 'Jack';

var sayName = person.sayName;

sayName.call(person); // 'Jason'

  上述代码中,如果先不看最后一行,看倒数第二行「var sayName = person.sayName」,通过上述的讲述,可以认定到这一行,如果直接调用,所处的执行上下文是全局变量window,但是最后一行的call函数,指定了this的对象为person,则输出‘Jason’。

  那么显式绑定是不是会出现绑定丢失呢?看下面的代码。

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
}

var name = 'Jack';

var Hi = function(fn) {
    fn(); // 'Jack'
};

Hi.call(person, person.sayName);

  乍一看最后一行的显式绑定,确实Hi函数的this绑定到了对象person上,Hi函数会接受一个参数fn,然后执行fn();此刻这个参数fn是call的第二个参数「person.sayName」,但是在执行fn的时候,相当于直接调用了sayName函数(person.sayName赋值给了参数fn,隐式绑定也丢了,),对应的是默认绑定。

  那我们能不能继续还是想要绑定到person上?

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
}

var name = 'Jack';

var Hi = function(fn) {
    fn.call(this); // 'Jason'
};

Hi.call(person, person.sayName);

  其实只用在Hi函数中对fn使用call调用,因为刚刚我们说了最后一句话,HI的this对象绑定到了person对象上,那么我们在调用fn函数的时候再次显示绑定一次this,此时「fn.call(this)」中的this就是Hi函数的this对象(person)。

(4)new绑定
  关于new会发生什么,可以具体看我之前的一篇文章(https://www.jianshu.com/p/6ea91eb41283
)。

  简单来说:
(1)创建一个空对象,作为将要返回的对象实例。
(2)将这个空对象的原型,指向构造函数的「prototype」属性。
(3)将这个空对象赋值给构造函数内的「this」关键字。
(4)开始执行构造函数内的代码。

function Person(name) {
    this.name = name;
}

var x = new Person('Jason');

console.log(x.name) // 'Jason'

(5)绑定例外
  上述(1)~(4)已经基本参数了this绑定的规则,但是我们还是要补充点可能存在的问题。比如「绑定例外」。

  如果我们在使用「显示绑定」的时候,第一个参数传了「null」或者「undefined」,这些值是会被忽略的,实际上应用的是「默认绑定」

(6)绑定优先级
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

(7)箭头函数
  箭头函数是ES6中的语法,带来了很多的便利,但是我们也有几个重要的注意点:
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
  其中第一点就跟本文的主题this密切相关,我们来看两个例子。

var name = 'Jason';

var obj = {
    name: 'Jack',
    sayName: function() {
        console.log(this.name); // 'Jack'
    }
};

obj.sayName();
var name = 'Jason';

var obj = {
    name: 'Jack',
    sayName: () => {
        console.log(this.name); // 'Jason'
    }
};

obj.sayName();

  上述两个例子非常相似,唯一不同点在于obj的sayName函数一个使用了普通函数形式(第一个例子),另一个使用了箭头函数(第二个例子),然后输出结果就不同了。

  总的来说:普通函数的this是函数「运行时」绑定的,而箭头函数的this是函数「定义时」绑定的。

  这里我们怎么理解「定义时」?我们可以说箭头函数的this和其外层代码库的this一样,或者说指向其父级执行上下文中的this

上述第二个例子中,箭头函数本身跟sayName平级以key:value的形式,也就是说箭头函数本身存在于obj对象上,而obj对象所处的环境在全局变量window上,所以此例中的this.name其实是window.name。我们再看两个例子。

var name = 'Jason';

function Test() {
    this.name = 'Jack';

    let fn = function() {
        console.log(this.name); // 'Jason'
    };

    fn();

}

var x = new Test();
var name = 'Jason';

function Test() {
    this.name = 'Jack';

    let fn = () => {
        console.log(this.name); // 'Jack'
    };

    fn();

}

var x = new Test();

  第一个例子中,一个普通函数赋值给了变量fn,然后直接调用fn(),是默认绑定,此时的this指向全局变量window,所以this.name就是window.name,输出‘Jason’。
  第二个例子中,我们把箭头函数赋值为了变量fn,箭头函数中的this指向了父级执行上下文中的this,父级执行上下文中的this是通过new指向了其构造函数的实例(本例中的x),然后Test函数中第一句的「this.name = 'Jack'」,使得x.name为Jack。

  我们再来一个例子练练手。

var obj = {
    hi: function(){
        console.log('1', this); // obj
        return ()=>{
            console.log('2',this); // obj
        }
    },
    sayHi: function(){
        return function() {
            console.log('3', this); // window
            return ()=>{
                console.log('4', this); // window
            }
        }
    },
    say: ()=>{
        console.log('5', this); // window
    }
}

let hi = obj.hi();

hi();

let sayHi = obj.sayHi();

let fun1 = sayHi();

fun1();

obj.say();

  我们来依次解释一下:
(1)第一条(1处)是由于「obj.hi()」调用函数,此时是隐式绑定,固然1处的this指向了obj。
(2)第2处,由于调用「hi()」,obj.hi()返回的是一个箭头函数,这个箭头函数中的this与外层代码库的this一样,外层代码库就是obj对象中的fn属性(这是一个函数),于是乎,跟1处的this是一样的,都指向obj。
(3)由于「obj.sayHi()」赋值给了变量sayHi,「sayHi」执行的时候,相当于原本是隐式绑定然后变为默认绑定,固然3处输出的是window。
(4)由于sayHi()赋值给了fun1,在「fun1()」执行的时候,执行的是一个箭头函数,然后我们就找这个箭头函数的外层代码库的this,就与3处的this相同,即输出window。
(5)由于「obj.say()」的执行,乍一看是一个隐式绑定,但是看到函数是箭头函数,obj的外层代码库所在的环境就是全局变量window,输出window。

那么箭头函数一定是静态的吗?

var obj = {
    hi: function(){
        console.log('1', this);
        return ()=>{
            console.log('2', this);
        }
    },
    sayHi: function(){
        return function() {
            console.log('3', this);
            return ()=>{
                console.log('4', this);
            }
        }
    },
    say: ()=>{
        console.log('5', this);
    }
}

let sayHi = obj.sayHi();

let fun1 = sayHi(); // window
fun1(); // window

let fun2 = sayHi.bind(obj)(); // obj
fun2(); // obj

  还是用上面的例子,我们看看fun1和fun2,第一次执行「sayHi()」赋值给fun1的时候,是由隐式绑定转换为了默认绑定,this为window,执行「fun1」的时候,是执行箭头函数,所以箭头函数里的this也是window。
  但是对于fun2, 「sayHi.bind(obj)()」中使用bind显示绑定了sayHi的this对象为obj,本来没有bind绑定的时候,是由隐式绑定转变为默认绑定,然后此处我们又强行使用bind绑定回来了。所以「sayHi.bind(obj)()」执行的时候,3处的this为obj,然后「fun2()」执行的时候,4处的this和3处的this一样都是obj。

我们来一个终极题目来综合一下。

var number = 5;
var obj = {
    number: 3,
    fn: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}

var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);

  我们来分析下这段代码,在obj对象的fn属性定义的时候,就是一个「立即执行函数」,并且带有「闭包」,我们看看这个「立即执行函数」中的this指向了谁?没有new绑定,没有显式绑定,没有隐式绑定,自然就是默认绑定了,this指向了全局变量window。所以「立即执行函数」中的代码可以这样理解。

var number; // number是undefined

window.number *= 2; // 此时全局变量中的number变为 10

number = number * 2; // 由于number是undefined, Number(undefined)为NaN,此处number变为NaN。
number = 3; // 然后又使变量number变为3

执行完fn的立即执行函数后。obj的fn属性是下列这样的:

fn: function() {
  var num = this.number;
  this.number *= 2;
  console.log(num);
  number *= 3;
  console.log(number);
}

然后我们执行「myFun.call(null)」,这种显式绑定我们在上述文章中说过,如果第一个参数为null,就是转为默认绑定。本例中就是执行obj的fn函数,然后我们再来每一行分析一波:我们首先想想obj的fn函数里面的this是谁?由于是默认绑定所以是window,于是乎。

var num = this.number; // 由于this指向了window,此时window.number为10,则num被赋值为10

this.number *= 2; // 使得全局变量的「number」,变为20

console.log(num); // 输出10

number *= 3; 这个number是obj.fn中的闭包函数中的「number」

console.log(number) ; // 同时输出这个number
改变闭包中的number变量

接着我们执行「obj.fn()」,还是优先执行了obj.fn的立即执行函数。立即执行函数的this还是指向全局变量window,然后依次分析每行代码。立即执行函数中:

var number;

this.number *= 2; // 等价于 window.number *= 2; 使得全局变量的number变为20

number = number * 2; // NaN

number = 3;

然后我们在执行「立即执行函数」返回的函数。此时由于是「obj.fn()」这样调用,所以this指向了obj对象。

var number = this.number; // 3,即obj.number

this.number *= 2; // 使得obj.number变为6

console.log(number); // 输出3

number *= 3; // 此时number还是指闭包中的那个「number」,刚刚number是9,现在就是27了

console.log(number); // 就是输出闭包那个「number」,是27

最后执行「console.log(window.number)」,此时全局变量的number是20,输出20.

参考:

  1. https://www.cnblogs.com/gaoht/p/10694967.html
  2. https://zhuanlan.zhihu.com/p/26475137
  3. http://es6.ruanyifeng.com/#docs/function
  4. http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
上一篇下一篇

猜你喜欢

热点阅读