《2w字大章 38道面试题》彻底理清JS中this指向问题(二)

简介: 《2w字大章 38道面试题》彻底理清JS中this指向问题(二)

5.显式绑定扩展


上面提了很多call/apply可以改变this指向,但都没有太多实用性。下面来一起学几个常用的call与apply使用。


题目5.1:apply求数组最值


JavaScript中没有给数组提供类似max和min函数,只提供了Math.max/min,用于求多个数的最值,所以可以借助apply方法,直接传递数组给Math.max/min


const arr = [1,10,11,33,4,52,17]
Math.max.apply(Math, arr)
Math.min.apply(Math, arr)
复制代码


题目5.2:类数组转为数组


ES6未发布之前,没有Array.from方法可以将类数组转为数组,采用Array.prototype.slice.call(arguments)[].slice.call(arguments)将类数组转化为数组。


题目5.3:数组高阶函数


日常编码中,我们会经常用到forEach、map等,但这些数组高阶方法,它们还有第二个参数thisArg,每一个回调函数都是显式绑定在thisArg上的。


例如下面这个例子


const obj = {a: 10}
const arr = [1, 2, 3, 4]
arr.forEach(function (val, key){
    console.log(`${key}: ${val} --- ${this.a}`)
}, obj)
复制代码


答案


0: 1 --- 10
1: 2 --- 10
2: 3 --- 10
3: 4 --- 10
复制代码


关于数组高阶函数的知识可以参考: JavaScript之手撕高阶数组函数


6.new绑定


使用new来构建函数,会执行如下四部操作:


  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

关于new更详细的知识,可以参考:JavaScript之手撕new

通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this。


题目6.1:new绑定


function User(name, age) {
    this.name = name;
    this.age = age;
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24);
console.log(zc.name)
复制代码


答案


zc
复制代码


题目6.2:属性加方法


function User (name, age) {
  this.name = name;
  this.age = age;
  this.introduce = function () {
    console.log(this.name)
  }
  this.howOld = function () {
    return function () {
      console.log(this.age)
    }
  }
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24)
zc.introduce()
zc.howOld()()
复制代码


这个题很难不让人想到如下代码,都是函数嵌套,具体解法是类似的,可以对比来看一下啊。


const User = {
  name: 'zc';
  age: 18;
  introduce = function () {
    console.log(this.name)
  }
  howOld = function () {
    return function () {
      console.log(this.age)
    }
  }
}
var name = 'Tom';
var age = 18;
User.introduce()
User.howOld()()
复制代码


  • zc.introduce(): zc是new创建的实例,this指向zc,打印zc
  • zc.howOld()(): zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18(阿包永远18)


答案


zc
18
复制代码


题目6.3:new界的天王山


new界的天王山,每次看懂后,没过多久就会忘掉,但这次要从根本上弄清楚该题。

接下来一起来品味品味:


function Foo(){
    getName = function(){ console.log(1); };
    return this;
}
Foo.getName = function(){ console.log(2); };
Foo.prototype.getName = function(){ console.log(3); };
var getName = function(){ console.log(4); };
function getName(){ console.log(5) };
Foo.getName();         
getName();        
Foo().getName();
getName();        
new Foo.getName();
new Foo().getName();
new new Foo().getName();
复制代码


  1. 预编译


GO = {
    Foo: fn(Foo),
    getName: function getName(){ console.log(5) };
}
复制代码


  1. 分析后续执行


  • Foo.getName(): 执行Foo上的getName方法,打印2
  • getName(): 执行GO中的getName方法,打印4
  • Foo().getName()
  • Foo()执行


// 修改全局GO的getName为function(){ console.log(1); }
getName = function(){ console.log(1) }
// Foo为默认绑定,this -> window
// return window
return this
复制代码


  • Foo().getName(): 执行window.getName(),打印1


  • getName(): 执行GO中的getName,打印1
  1. 分析后面三个打印结果之前,先补充一些运算符优先级方面的知识(图源:MDN)


image.png

从上图可以看到,部分优先级如下:new(带参数列表) = 成员访问 = 函数调用 > new(不带参数列表)


  1. new Foo.getName()


首先从左往右看:new Foo属于不带参数列表的new(优先级19),Foo.getName属于成员访问(优先级20),getName()属于函数调用(优先级20),同样优先级遵循从左往右执行。


  • Foo.getName执行,获取到Foo上的getName属性
  • 此时原表达式变为new (Foo.getName)()new (Foo.getName)()为带参数列表(优先级20),(Foo.getName)()属于函数调用(优先级20),从左往右执行
  • new (Foo.getName)()执行,打印2,并返回一个以Foo.getName()为构造函数的实例


这里有一个误区:很多人认为这里的new是没做任何操作的的,执行的是函数调用。那么如果执行的是Foo.getName(),调用返回值为undefinednew undefined会发生报错,并且我们可以验证一下该表达式的返回结果。


console.log(new Foo.getName())
// 2
// Foo.getName {}
复制代码


可见在成员访问之后,执行的是带参数列表格式的new操作。


  1. new Foo().getName()


  • 步骤4一样分析,先执行new Foo(),返回一个以Foo为构造函数的实例
  • Foo的实例对象上没有getName方法,沿原型链查找到Foo.prototype.getName方法,打印3


  1. new new Foo().getName()


从左往右分析: 第一个new不带参数列表(优先级19),new Foo()带参数列表(优先级20),剩下的成员访问和函数调用优先级都是20


  • new Foo()执行,返回一个以Foo为构造函数的实例
  • 在执行成员访问,Foo实例对象在Foo.prototype查找到getName属性
  • 执行new (new Foo().getName)(),返回一个以 Foo.prototype.getName()为构造函数的实例,打印3


  1. new Foo.getName()new new Foo().getName()区别:


  • new Foo.getName()的构造函数是Foo.getName
  • new new Foo().getName()的构造函数为Foo.prototype.getName


测试结果如下:


foo1 = new Foo.getName()
foo2 = new new Foo().getName()
console.log(foo1.constructor)
console.log(foo2.constructor)
复制代码


输出结果:


2
3
ƒ (){ console.log(2); }
ƒ (){ console.log(3); }
复制代码


通过这一步比较应该能更好的理解上面的执行顺序。


答案


2
4
1
1
2
3
3
复制代码


兄弟们,革命快要成功了,再努力一把,以后this都小问题啦。


7.箭头函数


箭头函数没有自己的this,它的this指向外层作用域的this,且指向函数定义时的this而非执行时。


  1. this指向外层作用域的this: 箭头函数没有this绑定,但它可以通过作用域链查到外层作用域的this
  2. 指向函数定义时的this而非执行时: JavaScript是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。更详细的介绍见JavaScript之静态作用域与动态作用域


题目7.1:对象方法使用箭头函数


name = 'tom'
const obj = {
    name: 'zc',
    intro: () => {
        console.log('My name is ' + this.name)
    }
}
obj.intro()
复制代码


上文说到,箭头函数的this通过作用域链查到,intro函数的上层作用域为window


答案


My name is tom
复制代码


题目7.2:箭头函数与普通函数比较


name = 'tom'
const obj = {
    name: 'zc',
    intro:function ()  {
        return () => {
            console.log('My name is ' + this.name)
        }
    },
    intro2:function ()  {
        return function() {
            console.log('My name is ' + this.name)
        }
    }
}
obj.intro2()()
obj.intro()()
复制代码


  • obj.intro2()(): 不做赘述,打印My name is tom
  • obj.intro()(): obj.intro()返回箭头函数,箭头函数的this取决于它的外层作用域,因此箭头函数的this指向obj,打印My name is zc


题目7.3:箭头函数与普通函数的嵌套


name = 'window'
const obj1 = {
    name: 'obj1',
    intro:function ()  {
        console.log(this.name)
        return () => {
            console.log(this.name)
        }
    }
}
const obj2 = {
    name: 'obj2',
    intro: ()=>  {
        console.log(this.name)
        return function() {
            console.log(this.name)
        }
    }
}
const obj3 = {
    name: 'obj3',
    intro: ()=> {
        console.log(this.name)
        return () => {
            console.log(this.name)
        }
    }
}
obj1.intro()()
obj2.intro()()
obj3.intro()()
复制代码


  • obj1.intro()(): 类似题目7.2,打印obj1,obj1
  • obj2.intro()(): obj2.intro()为箭头函数,this为外层作用域this,指向window。返回匿名函数为默认绑定。打印window,window
  • obj3.intro()(): obj3.intro()obj2.intro()相同,返回值为箭头函数,外层作用域introthis指向window,打印window,window


答案


obj1
obj1
window
window
window
window
复制代码


题目7.4:new碰上箭头函数


function User(name, age) {
    this.name = name;
    this.age = age;
    this.intro = function(){
        console.log('My name is ' + this.name)
    },
    this.howOld = () => {
        console.log('My age is ' + this.age)
    }
}
var name = 'Tom', age = 18;
var zc = new User('zc', 24);
zc.intro();
zc.howOld();
复制代码


  • zcnew User实例,因此构造函数Userthis指向zc
  • zc.intro(): 打印My name is zc
  • zc.howOld(): howOld为箭头函数,箭头函数this由外层作用域决定,且指向函数定义时的this,外层作用域为Userthis指向zc,打印My age is 24


题目7.5:call碰上箭头函数


箭头函数由于没有this,不能通过call\apply\bind来修改this指向,但可以通过修改外层作用域的this来达成间接修改


var name = 'window'
var obj1 = {
  name: 'obj1',
  intro: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  },
  intro2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}
obj1.intro.call(obj2)()
obj1.intro().call(obj2)
obj1.intro2.call(obj2)()
obj1.intro2().call(obj2)
复制代码


  • obj1.intro.call(obj2)(): 第一层函数为普通函数,通过call修改thisobj2,打印obj2。第二层函数为箭头函数,它的this与外层this相同,同样打印obj2
  • obj1.intro().call(obj2): 第一层函数打印obj1,第二次函数为箭头函数,call无效,它的this与外层this相同,打印obj1
  • obj1.intro2.call(obj2)(): 第一层为箭头函数,call无效,外层作用域为window,打印window;第二次为普通匿名函数,默认绑定,打印window
  • obj1.intro2().call(obj2): 与上同,打印window;第二层为匿名函数,call修改thisobj2,打印obj2


答案


obj2
obj2
obj1
obj1
window
window
window
obj2
复制代码


8.箭头函数扩展


总结


  • 箭头函数没有this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时。
  • 不可以用作构造函数,不能使用new命令,否则会报错
  • 箭头函数没有arguments对象,如果要用,使用rest参数代替
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。
  • 不能用call/apply/bind修改this指向,但可以通过修改外层作用域的this来间接修改。
  • 箭头函数没有prototype属性。


避免使用场景


  1. 箭头函数定义对象方法


const zc = {
    name: 'zc',
    intro: () => {
        // this -> window
        console.log(this.name)
    }
}
zc.intro() // undefined
复制代码


  1. 箭头函数不能作为构造函数


const User = (name, age) => {
    this.name = name;
    this.age = age;
}
// Uncaught TypeError: User is not a constructor
zc = new User('zc', 24);
复制代码


  1. 事件的回调函数

DOM中事件的回调函数中this已经封装指向了调用元素,如果使用构造函数,其this会指向window对象


document.getElementById('btn')
        .addEventListener('click', ()=> {
            console.log(this === window); // true
        })
复制代码


9.综合题


学完上面的知识,是不是感觉自己已经趋于化境了,现在就一起来华山之巅一决高下吧。


题目9.1: 对象综合体


var name = 'window'
var user1 = {
    name: 'user1',
    foo1: function () {
        console.log(this.name)
    },
    foo2: () => console.log(this.name),
    foo3: function () {
        return function () {
            console.log(this.name)
        }
    },
    foo4: function () {
        return () => {
            console.log(this.name)
        }
    }
}
var user2 = { name: 'user2' }
user1.foo1()
user1.foo1.call(user2)
user1.foo2()
user1.foo2.call(user2)
user1.foo3()()
user1.foo3.call(user2)()
user1.foo3().call(user2)
user1.foo4()()
user1.foo4.call(user2)()
user1.foo4().call(user2)
复制代码


这个题目并不难,就是把上面很多题做了个整合,如果上面都学会了,此题问题不大。


  • user1.foo1()、user1.foo1.call(user2): 隐式绑定与显式绑定
  • user1.foo2()、user1.foo2.call(user2): 箭头函数与call
  • user1.foo3()()、user1.foo3.call(user2)()、user1.foo3().call(user2): 见题目4.8
  • user1.foo4()()、user1.foo4.call(user2)()、user1.foo4().call(user2): 见题目7.5


答案:


var name = 'window'
var user1 = {
    name: 'user1',
    foo1: function () {
        console.log(this.name)
    },
    foo2: () => console.log(this.name),
    foo3: function () {
        return function () {
            console.log(this.name)
        }
    },
    foo4: function () {
        return () => {
            console.log(this.name)
        }
    }
}
var user2 = { name: 'user2' }
user1.foo1()  // user1
user1.foo1.call(user2) // user2
user1.foo2() // window
user1.foo2.call(user2) // window
user1.foo3()() // window
user1.foo3.call(user2)() // window
user1.foo3().call(user2) // user2
user1.foo4()() // user1
user1.foo4.call(user2)() // user2
user1.foo4().call(user2) // user1
复制代码


题目9.2:隐式绑定丢失


var x = 10;
var foo = {
   x : 20,
   bar : function(){
       var x = 30;
       console.log(this.x)
   }
};
foo.bar();
(foo.bar)();
(foo.bar = foo.bar)();
(foo.bar, foo.bar)();
复制代码


突然出现了一个代码很少的题目,还乍有些不习惯。


  • foo.bar(): 隐式绑定,打印20
  • (foo.bar)(): 上面提到过运算符优先级的知识,成员访问与函数调用优先级相同,默认从左到右,因此括号可有可无,隐式绑定,打印20
  • (foo.bar = foo.bar)():隐式绑定丢失,给foo.bar起别名,虽然名字没变,但是foo.bar上已经跟foo无关了,默认绑定,打印10
  • (foo.bar, foo.bar)(): 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10


上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足XXX.fn()格式,如果破坏了这种格式,一般隐式绑定都会丢失。


题目9.3:arguments(推荐看)


var length = 10;
function fn() {
    console.log(this.length);
}
var obj = {
  length: 5,
  method: function(fn) {
    fn();
    arguments[0]();
  }
};
obj.method(fn, 1);
复制代码


这个题要注意一下,有坑。


  • fn(): 默认绑定,打印10
  • arguments[0](): 这种执行方式看起来就怪怪的,咱们把它展开来看看:
  1. arguments是一个类数组,arguments展开,应该是下面这样:


arguments: {
    0: fn,
    1: 1,
    length: 2
}
复制代码


  1. arguments[0]: 这是访问对象的属性0?0不好理解,咱们把它稍微一换,方便一下理解:


arguments: {
    fn: fn,
    1: 1,
    length: 2
}
复制代码


  1. 到这里大家应该就懂了,隐式绑定,fn函数this指向arguments,打印2


题目9.4:压轴题(推荐看)


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);
复制代码


fn.call(null) 或者 fn.call(undefined) 都相当于fn()


  1. obj.fn为立即执行函数: 默认绑定,this指向window句一句的分析:


  • var number: 立即执行函数的AO中添加number属性,值为undefined
  • this.number *= 2: window.number = 10
  • number = number * 2: 立即执行函数AOnumber值为undefined,赋值后为NaN
  • number = 3: AOnumber值由NaN修改为3
  • 返回匿名函数,形成闭包


  1. 此时的obj可以类似的看成以下代码(注意存在闭包):


obj = {
   number: 3,
   fn: function () {
        var num = this.number;
        this.number *= 2;
        console.log(num);
        number *= 3;
        console.log(number);
    }
}
复制代码


  1. myFun.call(null): 相当于myFun(),隐式绑定丢失,myFunthis指向window。依旧一句一句的分析:


  • var num = this.number: this指向windownum = window.num = 10
  • this.number *= 2: window.number = 20
  • console.log(num): 打印10
  • number *= 3: 当前AO中没有number属性,沿作用域链可在立即执行函数的AO中查到number属性,修改其值为9
  • console.log(number): 打印立即执行函数AO中的number,打印9


  1. obj.fn(): 隐式绑定,fnthis指向obj继续一步一步的分析:


  • var num = this.number: this->objnum = obj.num = 3
  • this.number *= 2: obj.number *= 2 = 6
  • console.log(num): 打印num值,打印3
  • number *= 3: 当前AO中不存在number,继续修改立即执行函数AO中的numbernumber *= 3 = 27
  • console.log(number): 打印27


  1. console.log(window.number): 打印20


这里解释一下,为什么myFun.call(null)执行时,找不到number变量,是去找立即执行函数AO中的number,而不是找window.number: JavaScript采用的静态作用域,当定义函数后,作用域链就已经定死。(更详细的解释文章最开始的推荐中有)


答案


10
9
3
27
20
复制代码


总结


  • 默认绑定: 非严格模式下this指向全局对象,严格模式下this会绑定到undefined
  • 隐式绑定: 满足XXX.fn()格式,fnthis指向XXX。如果存在链式调用,this永远指向最后调用它的那个对象
  • 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
  • 显示绑定: 通过call/apply/bind修改this指向
  • new绑定: 通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
  • 箭头函数绑定: 箭头函数没有this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时


后语


this到这里基本接近尾声了,松了一口气。 这篇文章写了好久,找资源,修改博文,各种乱七八糟的杂事,导致迟迟写不出满意的博文。有可能天生理科男的缘故吧,怎么写感觉文章都很生硬,但好在还是顺利写完了。


在文章的最后,感谢一下参考的博客和题目的来源



最后按照阿包惯例,附赠一道面试题:


var num = 10
var obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)
复制代码


相关文章
|
3月前
|
JavaScript 前端开发
常见的JS面试题
【8月更文挑战第5天】 常见的JS面试题
61 3
|
19天前
|
JSON JavaScript 前端开发
[JS]面试官:你的简历上写着熟悉jsonp,那你说说它的底层逻辑是怎样的?
本文介绍了JSONP的工作原理及其在解决跨域请求中的应用。首先解释了同源策略的概念,然后通过多个示例详细阐述了JSONP如何通过动态解释服务端返回的JavaScript脚本来实现跨域数据交互。文章还探讨了使用jQuery的`$.ajax`方法封装JSONP请求的方式,并提供了具体的代码示例。最后,通过一个更复杂的示例展示了如何处理JSON格式的响应数据。
29 2
[JS]面试官:你的简历上写着熟悉jsonp,那你说说它的底层逻辑是怎样的?
|
1月前
|
Web App开发 JavaScript 前端开发
前端Node.js面试题
前端Node.js面试题
|
3月前
|
存储 JavaScript 前端开发
2022年前端js面试题
2022年前端js面试题
40 0
|
3月前
|
JavaScript 前端开发 程序员
JS小白请看!一招让你的面试成功率大大提高——规范代码
JS小白请看!一招让你的面试成功率大大提高——规范代码
|
3月前
|
JavaScript 前端开发 UED
小白请看! 大厂面试题 :如何用JS实现瀑布流
小白请看! 大厂面试题 :如何用JS实现瀑布流
|
3月前
|
存储 JavaScript 前端开发
JS浅拷贝及面试时手写源码
JS浅拷贝及面试时手写源码
|
3月前
|
JavaScript 前端开发
JS:类型转换(四)从底层逻辑让你搞懂经典面试问题 [ ] == ![ ] ?
JS:类型转换(四)从底层逻辑让你搞懂经典面试问题 [ ] == ![ ] ?
|
4月前
|
缓存 JavaScript 前端开发
js高频面试题,整理好咯
中级前端面试题,不低于12k,整理的是js较高频知识点,可能不够完善,大家有兴趣可以留言补充,我会逐步完善,若发现哪里有错,还请多多斧正。
|
4月前
|
JavaScript
JS【详解】setTimeout 延时(含清除 setTimeout,计时开始时间,0 秒延时解析,多 setTimeout 执行顺序,setTimeout 应用场景,网红面试题)
JS【详解】setTimeout 延时(含清除 setTimeout,计时开始时间,0 秒延时解析,多 setTimeout 执行顺序,setTimeout 应用场景,网红面试题)
923 0
下一篇
无影云桌面