前言
这篇文章主要来解析牛客笔试题部分的 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
,打印3
fun()
: 隐式绑定丢失: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
变量,打印0
console.log(this.a)
:new
绑定this
指向新的实例对象,当前题目没有给实例对象添加a
属性,打印undefined
console.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
,打印undefined
obj2
隐式绑定丢失: 打印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
打印foo
bar
是new obj.foo()
的实例,console.log(bar.name)
打印foo
setTimeout
异步任务,等到同步执行完毕再来调用它的回调bar3 = bar2 = bar
,将bar2,bar3,bar
的地址都指向bar
所指向的空间。bar2.name = 'foo2'
,修改地址指向堆内存的值console.log(bar3.name)
: 由于三个变量指向同一块地址,bar3
修改了name
,bar3
也随之改变,打印foo2
setTimeout
的回调执行,打印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 -> window
this.name
相当于window.name
,修改了全局的name
变量值,打印foo
setTimeout
回调等同步代码执行完毕- 全局的
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指向问题