前言
这篇文章主要来解析牛客笔试题部分的 this 指向题目。首先我们先从宏观上了解一下 JavaScript 中得 this 指向问题。
JavaScript 中 this 共有四种绑定(包括隐式绑定丢失)加 ES6 新增得箭头函数绑定,如果把这几种绑定全学会了,this 指向完全不成问题。 this 指向更详细的讲解,可以去看前面写过的一篇博文,里面还有 38 道题目,详细完整的讲解了 this 指向问题。
传送门: 《2w字大章 38道面试题》彻底理清JS中this指向问题
- 默认绑定: 非严格模式下
this指向全局对象,严格模式下this会绑定为undefined - 隐式绑定: 满足
XXX.fn()格式,fn的this指向XXX。如果存在链式调用,this永远指向最后调用它的那个对象 - 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
- 显式绑定: 通过
call/apply/bind修改this指向 - new绑定: 通过
new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this。 - 箭头函数绑定: 箭头函数没有
this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时
题目一. 隐式绑定与隐式绑定丢失
var x = 1; var obj = { x: 3, fun:function () { var x = 5; return this.x; } }; var fun = obj.fun; console.log(obj.fun(), fun()); 复制代码
解析
JavaScript 对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。fun = obj.fun 相当于将 obj.fun 指向得堆内存指针赋值给了 fun,此后 fun 执行与 obj 不会有任何关系,发生隐式绑定丢失。
obj.fun(): 隐式绑定,fun里面的this指向obj,打印3fun(): 隐式绑定丢失:fun默认绑定,非严格模式下,this指向window,打印1
答案
3 1 复制代码
题目二: 隐式绑定丢失
var person = { age: 18, getAge: function() { return this.age; } }; var getAge = person.getAge console.log(getAge()) 复制代码
简单的隐式绑定丢失问题
答案
undefined 复制代码
题目三: 隐式绑定丢失
var obj = { name:"zhangsan", sayName:function(){ console.log(this.name); } } var wfunc = obj.sayName; obj.sayName(); wfunc(); var name = "lisi"; obj.sayName(); wfunc(); 复制代码
简单的隐式绑定问题,不多做赘述了。
答案
zhangsan undefined zhangsan lisi 复制代码
题目四:new绑定
var a = 5; function test() { a = 0; console.log(a); console.log(this.a); var a; console.log(a); } new test(); 复制代码
解析
使用new来构建函数,会执行如下四部操作:
- 创建一个空的简单
JavaScript对象(即{}); - 为步骤1新创建的对象添加属性
__proto__,将该属性链接至构造函数的原型对象 ; - 将步骤1新创建的对象作为
this的上下文 ; - 如果该函数没有返回对象,则返回
this。
通过 new 来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的 this 。
console.log(a): 打印变量a的值,当前testAO中存在a变量,打印0console.log(this.a):new绑定this指向新的实例对象,当前题目没有给实例对象添加a属性,打印undefinedconsole.log(a): 同第一个,打印0
答案
0 undefined 0 复制代码
题目五:箭头函数与显式绑定
function fun () { return () => { return () => { return () => { console.log(this.name) } } } } var f = fun.call({name: 'foo'}) var t1 = f.call({name: 'bar'})()() var t2 = f().call({name: 'baz'})() var t3 = f()().call({name: 'qux'}) 复制代码
- 箭头函数没有
this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时。 - 箭头函数,不能通过
call\apply\bind来修改this指向,但可以通过修改外层作用域的this来达成间接修改。 JavaScript是静态作用域,即函数的作用域在函数定义的时候就决定了,而箭头函数的this是通过作用域链查到的,因此箭头函数定义后,它的作用域链就定死了。
f = fun.call({name: 'foo'}): 将fun函数的this指向{name: 'foo'},并返回一个箭头函数,因此箭头函数的this也指向{name: 'foo'}t1 = f.call({name: 'bar'})()(): 对第一层箭头函数执行call操作,无效,当前this仍指向{name: 'foo'},第二层、第三层都是箭头函数,第三层的this也指向{name: 'foo'},打印foo- 后续
t2 t3分别对第二层、第三层箭头函数使用call,无效,最终都打印foo。
答案
foo foo foo 复制代码
题目六:箭头函数
let obj1 = { a: 1, foo: () => { console.log(this.a) } } // log1 console.log(obj1.foo()) const obj2 = obj1.foo // log2 console.log(obj2()) 复制代码
解析
obj1.foo为箭头函数,obj1为对象,无法提供外层作用域,因此obj.foo里面的this指向window
obj1.foo(): 箭头函数,this指向window,打印undefinedobj2隐式绑定丢失: 打印undefined
答案
undefined undefined 复制代码
题目七: 综合题(推荐看)
var name = 'global'; var obj = { name: 'local', foo: function(){ this.name = 'foo'; console.log(this.name); }.bind(window) }; var bar = new obj.foo(); setTimeout(function() { console.log(window.name); }, 0); console.log(bar.name); var bar3 = bar2 = bar; bar2.name = 'foo2'; console.log(bar3.name); 复制代码
解析
这个题的整体出题质量还是挺高的,首先咱们来把涉及到的知识罗列一下:
bind是显式绑定,会修改this指向,但bind()函数不会立即执行函数,会返回一个新函数setTimeout是异步任务,同步任务执行完毕后才会执行异步任务- 绑定优先级: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
解析
obj.foo将它的this通过bind显式的绑定为window,但bind不会立即执行var bar = new obj.foo():new绑定优先级大于bind,因此bind失效了,此时this指向new实例,因此obj.foo内部的console打印foobar是new obj.foo()的实例,console.log(bar.name)打印foosetTimeout异步任务,等到同步执行完毕再来调用它的回调bar3 = bar2 = bar,将bar2,bar3,bar的地址都指向bar所指向的空间。bar2.name = 'foo2',修改地址指向堆内存的值console.log(bar3.name): 由于三个变量指向同一块地址,bar3修改了name,bar3也随之改变,打印foo2setTimeout的回调执行,打印global
答案
foo foo foo2 global 复制代码
题目八: 综合题目七修改
题目六虽然出的很有质量,但是我感觉有几个地方考察的让人不满足,咱们来改一下题目。
改法一: 去除 new 绑定
var name = 'global'; var obj = { name: 'local', foo: function(){ this.name = 'foo'; console.log(this.name); }.bind(window) }; obj.foo(); setTimeout(function() { console.log(this.name); }, 0); console.log(name); 复制代码
obj.foo没有做修改,它的this通过bind显式的绑定为window,但bind不会立即执行obj.foo()执行,显式绑定优先级大于隐式绑定,因此此时foo函数this -> windowthis.name相当于window.name,修改了全局的name变量值,打印foosetTimeout回调等同步代码执行完毕- 全局的
name已经修改为foo,打印foo - 执行
setTimeout的回调,默认绑定,打印foo
答案
foo foo foo 复制代码
改法一: 修改 bind 为 call
var name = 'global'; var obj = { name: 'local', foo: function(){ this.name = 'foo'; console.log(this.name); } }; obj.foo.call(window); var bar = new obj.foo(); setTimeout(function() { console.log(window.name); }, 0); console.log(bar.name); var bar3 = bar2 = bar; bar2.name = 'foo2'; console.log(bar3.name); 复制代码
call 与 bind 的区别就在于 call 函数会立即执行
因此 obj.foo.call(window) 会立即执行函数,同时也修改了全局的 name 值。其他部分与上面所讲类似,不多做赘述了。
答案
foo foo foo foo2 foo 复制代码
往期精彩文章
- 牛客最新前端JS笔试百题
- JavaScript之彻底理解原型与原型链
- JavaScript之预编译学习
- JavaScript之彻底理解EventLoop
- 《2w字大章 38道面试题》彻底理清JS中this指向问题