意思的文学
曾在网上看到过这样一个笑话,说某老外苦学汉语十年,到中国参加汉语考试,试题如下:请解释下文中每个“意思”的意思。
看到这个笑话让我不禁想起,作为前端的你,是否经常遇到过面试中某种常见的考题,例如请指出以下代码的所有输出结果?
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不会不知其意思啦。哈哈哈哈,大家白白,咱们下次接着聊。