大话this之玄学

384 阅读7分钟

意思的文学
曾在网上看到过这样一个笑话,说某老外苦学汉语十年,到中国参加汉语考试,试题如下:请解释下文中每个“意思”的意思。

看到这个笑话让我不禁想起,作为前端的你,是否经常遇到过面试中某种常见的考题,例如请指出以下代码的所有输出结果?

var name = 'window'

function Person(name) {
    this.name = name
    this.student = {
        name: 'obj',
        hello1: function () {
            return function () {
                console.log(this.name)
            }
        },
        hello2: function () {
            return () => {
                console.log(this.name)
            }
        }
    }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.hello1()()
person1.obj.hello1.call(person2)()
person1.obj.hello1().call(person2)

person1.obj.hello2()()
person1.obj.hello2.call(person2)()
person1.obj.hello2().call(person2)

如果你能快速有效的给出正确答案,那么恭喜,你对this玄学研究的非常透彻了。如果你眼花缭乱,无妨!让我们一起进阶了解一下this的正确食用方式。

1.this从何而来

作为JavaScript中的关键字之一,为何this成为了面试官争相询问的必要考题。理解并掌握的攻城狮们能够清楚的一一指出,而一知半解的猿儿们都把它看成了一种玄学,this不知所以,一通箭头函数和bind保this。
首先我们需要思考,this的出现是为了解决什么问题呢?
我们都知道this是一个指针,指向调用函数的对象,如果没有this,所有函数中都将使用一个具体的对象值来表示,如下代码:

var person = {
    name: "CassieLala",
    age: "8",
    grade: 2,
    hello: function () {
        console.log(`Hello, my name is ${person.name},
        I am ${person.age} years old, and I am in Grade ${person.grade}`);
    }
}
person.hello()

我们观察可以发现,当我们想要修改对象名称时,所有对象方法中引用对象字段的地方都需要统一修改,维护麻烦。这个时候如果有一个对象,永远指向当前对象,比如他叫this?!!是不是一切就迎刃而解了。 那么今天,我们就通过实践来看看this的终极对象绑定的是谁!

2.this绑定规则

在弄清楚this的具体绑定对象是谁之前,我们先来了解一下this的绑定规则有哪些。
this的绑定规则大体分为4种:默认绑定、隐式绑定、显示绑定和new绑定。接下来我们逐一看看他们的绑定方式及优先级。

1. 默认绑定

默认绑定的定义为:当没有其他默认规则可使用时产生的默认规则(一般是独立函数的调用)。我们可以理解为,函数没有被绑定到某个具体对象上进行的调用。 举个例子:

function hello() {
  console.log(this);
}
var name = "window";
hello(); // window

当我们在浏览器环境下打印函数的时候,输出结果为window。这是因为非严格模式下,全局作用域中的this指向window,而name也挂载在全局window上。 此上结果及下文中出现的结果,未特殊说明均为非严格模式下浏览器环境。严格模式下,未声明对象会报错;node环境下全局this为undefined。

2. 隐式绑定

隐式绑定的定义为:指定某个对象调用方法,即存在执行上下文。 举个例子1:

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

var name = 'window';

var person = {
  name: "CassieLaLa",
  hello: hello
}

person.hello(); // person

通过以上例子我们不难发现,虽然hello方法是定义在全局的,但是他的指针对象在调用方法的person身上,这就是我们所说的执行上下文概念,最终调用方法的对象是谁,方法内部的this便是谁,哪怕存在对象调用链,我们只要观察最终最后执行对象即可。例如以下例子2:

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

var name = 'window';

var gril = {
  name: "gril",
  hello: hello
}

var student = {
  name: "student",
  gril: gril
}

var person = {
  name: "student",
  student: student
}

person.student.gril.hello(); //gril

如上所示,不论前面的对象调用链有多长,只要最终hello的执行对象是谁,hello中的this就指向谁。 隐式绑定存在指针丢失风险:当方法的引用被赋值时,赋值前的调用对象将丢失。如下例子3:

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

var name = 'window';

var person = {
  name: "student",
  hello: hello
}

var morning = person.hello;
morning(); // window

虽然看似person.hello赋值的时候,跟上述场景一有点例子2的写法有点类似,但我们思其根本,赋值给morning的hello这个方法的引用,而真正调用hello方法的是morning,morning挂载在window上的,所以此处的person对象将丢失,结果输出为window。

3. 显示绑定

上面例子描述我们发现,隐式绑定容易造成对象丢失,那我们如何操作,可以使所需的对象不被遗失呢?答案就是显示绑定!
显示绑定字如其名,就是我给你对象,你就绑定啥对象,我将需要绑定的对象告诉你!哈哈哈,这不能再丢了吧?!
这里我们将请出JavaScript中三大指针保卫神将:call,apply和bind。直接看以下例子:

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

var name = 'window';

var person = {
  name: "student",
  hello: hello
}

var morning = person.hello;
var afternoon = person.hello;
var goodnight = person.hello;
morning.call(person); // student
afternoon.apply(person); // student
goodnight.bind(person)(); // student

这里对于三大神将的方法介绍就不过多介绍了,不过要注意bind与call、apply的使用区别哦,call和apply直接调用方法即执行,而bind仅对象绑定,仍需加上()来执行函数。

4. new绑定

JavaScript与其他语言有所不同,他的class类其实只是一个类的概念,本质还是对函数的构造函数调用。不知道这样描述是不是有点迷糊,我们来看下以下的例子:

function Person(name) {
  this.name = name;
  console.log(this.name); // CassieLala
}

var name = 'window';

var person = new Person('CassieLala');
console.log('Hello,', person.name);  // Hello, CassieLala

从上述例子我们可以看出,如果我们除却new绑定的说法,按理此处应该套用隐式绑定的说法,person由于没有具体的对象绑定,从而this指向window。但此处new的作用,就是创建了一个全新的对象,且这个对象会被Prototype连接并绑定到函数调用的this上去,当函数内部没有其他对象返回时,新对象将被返回。故而函数内部name最终被调用的结果为CassieLala。

3.绑定规则中孰优?

我们上面分别介绍了四种this的绑定规则,那么这四种绑定规则中,优先级如何呢?我们先买个关子,通过例子见分晓:

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

var name = 'window';

var person = {
    name: "person",
    hello: hello
}

var student = {
    name: "student",
    hello: hello
}

var gril = {
    name: "gril",
    hello: hello
}

// 隐式 -- 隐式绑定 > 默认绑定
person.hello(); // person
student.hello(); // student

// 显示+隐式 -- 显示绑定 > 隐式绑定 > 默认绑定
person.hello.call(student); // student
student.hello.bind(person)(); // person

// new+隐式 -- new > 隐式绑定 > 默认绑定
new person.hello(); // hello对象
// new+隐式 -- 报错,new与显示方法不可公用
var hello = new hello.call(obj)
// 先显示再new
var girl = hello.bind(student);
new girl(); // hello对象, 说明使用的是new绑定

通过上述代码测试,我们不难看出,四种绑定规则的优先级依次为:
new > 显示绑定 > 隐式绑定 > 默认绑定

4.箭头函数

当然,除了以上我们所说的四种绑定方式,es6中还提供了一种简单直接的this绑定方式:箭头函数,箭头函数保证this总是当前环境上级环境对象。
常见的使用场景如:setTimeout、forEach等相关异步函数。如下例:

var name = 'window';
var person = {
    name: 'person',
    hello: function () {
        setTimeout(function () {
            console.log(this.name);
        }, 0);
    },
    morning: function () {
        setTimeout(()=> {
            console.log(this.name);
        }, 0);
    }
}

person.hello() // window
person.morning() // person

由上述例子可看出,hello中使用的是普通函数,setTimeout自身方法内部指针指向window对象。而morning中使用箭头函数以后,其使用当前环境上级环境对象,即person对象,故而输出结果为person。

5.思考

最后,我们来一起思考一下,为什么vue中的方法只能写成普通函数方法而不能使用箭头函数?

export default {
    data(){
        return {
            name: 'CassieLala'
        }
    },
    methods: {
        hello(){
            console.log(this.name); // CassieLala
        },
        morning: ()=>{
            console.log(this.name); // undefined
        }
    }
}

其实这跟vue的源码实现有关,vue中的methods对象中的所有函数方法都被遍历,并通过bind绑定了publicThis对象,而data中的this指针也是这个对象,所以我们可以在方法中直接通过this.name访问到data中的name值。如果将普通方法变成了箭头函数,当前作用域没有this,就只能去上层作用域查找,而最终找到window对象。

6.完结撒花~~~

以上就是我们大话this玄学的所有内容了,不知道看到这里的猿儿们有没有得到进化和飞升,希望以后大家看到这个this,那个this不会不知其意思啦。哈哈哈哈,大家白白,咱们下次接着聊。