别低估自己,但,这道题,真的有点难

4,270 阅读9分钟

今天一个朋友转给我一道题,让我帮忙解释解释。

当我看到题目的时候,第一眼觉得贼简单,但是看提问越到后面越懵逼了,在琢磨着能不能猜对了…………

var age = 10;
var person={
   age:20,
   getAge(){
       var age = 30;
       return this.age;
    },
};
alert(age,age*2);

person.getAge(); 

var b = person.getAge;
b(); 

(person.getAge)(); 

(1,person.getAge)();

(1,person.getAge.bind(person))();

(person.getAge,person.getAge)();

(person.getAge=person.getAge)();

person.getAge.call(); 

person.getAge.call(person);

function getAge2({
    this.age = 40;
    console.log(person.getAge());
};
getAge2();
console.log(age);

function getAge3(){
    this.age = 50;
    this.getAge4 = ()=>{
        console.log(person.getAge.call(this));
    }
}
new getAge3().getAge4();
console.log(age);

function getAge4(){
    this.age = 60;
    this.getAge5 = ()=>{
    console.log(person.getAge.call(this));
    }
}
new getAge4().getAge5();
console.log(age);

var age2 = 10;
var person2={
   age2:20,
   getAge2:()=>{
       var age2 = 30;
       return this.age2;    
    },
};
console.log(person2.getAge2.call());
console.log(person2.getAge2.call(person2));

果不其然,我答错了好多……

这道题目,题简单的不能再简单了,就是对象,函数,变量,但是问的很深,没有扎实的知识,很难确切的回答上这些问题。

要回答这些问题,关键还是要深入了解 this 和 逗号表达式。

首先我们简单回顾下这两个非常重要的知识。最后再看看文末的综合题目。

this

this 在 js 中非常重要。在笔试的时候,如果有考察基础知识,那么出现 this 的概率那可不是一般的高。

普通函数

一句话:谁调用就指向谁。

var person={
   age:20,
   getAge(){
       var age = 30;
       return this.age;
    },
};
person.getAge(); // 20

这个的 getAge 方法是 person 调用的,所以 this 指向 person,person.age 输出为 20;

箭头函数

一句话:调用者指向谁,则指向谁。

var age = 10;
var person={
   age:20,
   getAge:()=>{
       var age = 30;
       return this.age;
    },
};
person.getAge(); // 10

这个的 getAge 方法是 person 调用的,则 getAge 和 person 的指向一致,person 是 window 调用的(参照上述普通函数),所以 person 指向 window,因此 getAge 也指向 window,输出 10。

强制改变 this 指向

一句话:你说指向谁就指向谁。

改变 this 指向,有 call,apply,bind 这几种方法。

var age = 10;
var person={
   age:20,
   getAge:function(){
       var age = 30;
       return this.age;
    },
};
person.getAge.call(person);

这里在执行 getAge 方法的时候,传入了 person,那么 getAge 的 this 指向 person,所以输出 20。

逗号表达式

逗号表达式(Comma Operator)

(http://www.ecma-international.org/ecma-262/#sec-abstract-operations)

逗号表达式 可以用于分割任何一个表达式,可以用于分割函数参数等。

function test(){
    let a=1;
    return ++a,a++,a++;
}
console.log(test());

这里逗号用于分割表达式,等价于:

function test(){
    let a=1;
    ++a;
    a++;
    return a++;
}
console.log(test());

自然,答案不用多说,是 3 ,因为 return 后面的是 a++,如果是 ++a ,那结果是 4(这里不太明白的,自行学习 “++运算符”)。

再看原题

接下来我们来一一解答上面的问题,我再贴一下题目。

var age = 10;
var person={
   age:20,
   getAge(){
       var age = 30;
       return this.age;
    },
};

alert(age,age*2)

这里逗号分隔的是参数列表(Argument Lists),因为 alert 只接受一个参数,即第一个参数,后面的都忽略。因此弹窗是 10。

var b = person.getAge; b();

这个输出结果是 10,主要是要区别 person.getAge(),差别在于当前是先赋值给一个变量 b,然后执行 b()。

等价于:

var b = person.getAge; 
window.b(); 

回到最前面的知识点,[谁调用,指向谁],这里赋值变量之后,调用 b 方法的是 window,所以 this 指向 window,答案是 10。

(person.getAge)();

这里括号只是起到一个分割的作用,并没有实际意义,等价于 person.getAge()。所以答案是 20;

(1,person.getAge)();

这里和上一个题目的差别是引入了逗号表达式。我们知道逗号表达式返回的是最后一个值,即 person.getAge,注意这里是表达式返回值。

等价于:

var a = (1,person.getAge);
a();

或者:

var a = (false||person.getAge);
a();

显然,这里 a 调用方是 window,所以答案是 10。注意这里是非严格模式下。

后面题目如果牵涉到模式区别的时候通常都是指非严格模式,非严格模式与严格模式下的重要区别是当 this 为 null 或者 undefined 的时候,是否会改为指向 window。 非严格模式下会改为指向 window,严格模式就仍然为保持 null 或者 undefined。

"use strict"
var age = 10;
var person={
   age:20,
   getAge(){
       var age = 30;
       return this.age;
    },
};
console.log(person.getAge());// 输出仍然是 20,this 指针指向 person
var a = (1,person.getAge);
a();//  Cannot read property 'age' of undefined , this 是 undefined

(1,person.getAge.bind(person))();

这道题是 逗号表达式 和 bind 知识点,参照上面的分析,等价于:

var a =(1,person.getAge.bind(person));
a();

这里与上一道的区别是,返回的是一个 bind 之后的函数,a 方法已经强制指向 person 了,所以等价于:

person.getAge.bind(person)()

答案为 20,this 指向 person。

(person.getAge,person.getAge)();

这个其实就是一个逗号表达式,返回最后一个项的值,这里连续设置两个 person.getAge,只是一个陷阱,前面的 person.getAge 对结果没影响。咬定青山山不放松,坚定排除陷阱。

等价于:

(1,person.getAge)();

答案与上面一致,为 10。

(person.getAge=person.getAge)();

这里有点不一样,意思为:对象 person,给它设置了一个属性 getAge(如果有 getAge 属性,则重新赋值),将这个属性 getAge 用 person.getAge 赋值。

括号里面是一个赋值表达式,表达式的返回值,就是这个重新被赋值了 person.getAge 的 person 对象下面的 getAge 属性。

等价于:

var person.getAge = person.getAge // 赋值
var a = person.getAge;  // 表达式返回值
a();

上面已经解释,所以输出为 10。

person.getAge.call();person.getAge.call(person);

这里使用 call 函数改变 this 指针。在不传作用域参数的时候,在严格模式下 this 是 undefined,这里是非严格模式。

call,apply,bind 都可以改变 this 指针,如下:

// 10  call 为空,this 为 undefined,重新指向为 window
person.getAge.call();
// 20 this 指针指向 person
person.getAge.call(person); 

末尾大题

题目 1

function getAge2({
    this.age = 40;
    console.log(person.getAge());// 20
};
getAge2();
console.log(age);// 40

里面的 person.getAge 仍然是谁调用执行谁,person.getAge 方法的 this 是指向 person,输出是 20。 但是里面有一个 this.age = 40 的赋值。

那么这个 this 是啥?当然也是谁调用指向谁,这里的 getAge2 是 window 调用,所以这里的 this 是 window。

那么这相当于将外层的 age 已经修改为 40 了,所以 console.log(age) ,已经变成 40 了。

题目 2

function getAge3(){
    this.age = 50;
    this.getAge4 = ()=>{
        console.log(person.getAge.call(this));// 50
    }
}
new getAge3().getAge4();
console.log(age); // 40

在 getAge3 里面,定义了一个公有方法 getAge4,但是这里是一个箭头函数,里面使用 call 修改函数 person.getAge 的 this 指针指向当前的 this,那么当前 this 指向哪里呢?

仍然是看谁调用了 getAge4,这里是 new getAge3() 这个实例调用了 getAge4,所以 call 传进去的 this 是指向 new getAge3(),这里 this.age=50 被赋值为 50 了,所以输出为 50。

但是全局的 age 并没有被修改,与 题1 不一样,这里的 this 指向了实例对象,并不是指向 window,所以全局的 age 仍然为 40。

题 3

当读到这里的时候,你或许已经豁然开朗,觉得很理解了,那么这一题你会做吗?

var age2 = 10;
var person2={
   age2:20,
   getAge2:()=>{
       var age2 = 30;
       return this.age2;
    },
};
console.log(person2.getAge2.call()); // 10
console.log(person2.getAge2.call(person2)); // 10

这里的核心差别是 getAge2 函数是使用了箭头函数。按上面理解的,箭头函数的 this 指向 person2 ?

错了!不是如此。

实际上是箭头函数没有自己的 this,既然没有,那 call 怎么能改变它自身的 this 指针呢?

参照文档:(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)

所以上面不管是 call(),还是 call(person2),都无法修改 this 指针,所以两者都输出全局的 10。

End

我们再来看一道题,箭头函数的 this 。

var age = 10;
var person={
   age:20,
   child:{
    age:40,
    getAge:function(){
       return this.age;
    },
   },
   child2:{
    age:40,
    getAge:()=>{
       return this.age;
    },
   },
   child3:function(){
    this.getAge = ()=>{
        return this.age;
    }
   }
};
console.log(person.child.getAge());// 40
console.log(person.child2.getAge()); // 10
console.log((new person.child3()).getAge()); // undfined

根据我们上面的知识,可以知道 person.child.getAge() 的 this 就是谁调用指向谁,这里是 child 调用,所以指向 child ,输出 40 。

我们知道,箭头函数是没有自己的 this,那么它的 this 是啥呢?

就是当前箭头函数逐级向上查找,找到函数作用域的 this,则为当前箭头函数的 this。

所以,这里 person.child2.getAge 函数的父级调用方是 child2,但是 child2 是对象,也没有自己的 this 和 作用域,所以继续向上查找 person,然后发现 person 也是对象,再继续向上查找,找到 window 这个大 Boss 了,所以 this 就指向 window ,输出为 10。

(new person.child3()).getAge() 的 this ,同理向上一级查找,发现 new person.child3() 是个函数实例,所以 this 指向 child3 的这个实例,然而 child3 实例没有 age 属性,所以输出 undefined。

这些题,确实很难!如果有不理解的,多看几篇解答,相信你一定会豁然开朗!或者关注公众号,加作者微信,一对一解答。

欢迎关注我的微信公众号,一起做靠谱前端!

follow-me