重温面试常客,JS基础学习之 this
在我们面试时,面试公司经常会给我们一份笔试题目,虽然大家都不想写,但是多数时候不得不乖乖的就范,笔试题里经常就有一些,这类的题目:
function test() {
console.log(this.a)
}
var obj = {
a: 10,
test: test
}
var obj1 = {
a: 20
}
var a = 30
test() // 30
obj.test() // 10
test.call(obj1) // 20
复制代码
上面的例子是为了考察this的指向问题,例子属于比较简单的,面试时的题目可能会比这复杂很多,但是没事,正所谓万变不离其宗,只要对this够熟悉,那么遇到更难的题目也可以从容应对。
什么是this
this 是当前环境执行期上下文对象的一个属性,是JS中的一个关键字,它是一个很特别的关键字,被自动定义在所有的函数作用域中,它指向的问题使它成为Js中最复杂的机制之一,而且在严格模式和非严格模式之间也会有一些差别。
this的指向
this的指向,简单的总结就是,谁调用了this的宿主,那么this就指向谁,这是它的基本原则;因此我们想要知道this指向哪里,那么就需要找到它的调用位置;判断调用位置的规则可以使用以下四点。
默认绑定规则
默认绑定规则既是使用独立函数调用,这个应该是最常用的函数调用方式了,也是最简单的一种方式,代码如下:
function test() {
console.log(this.a)
}
var a = 30
test() // 30
复制代码
test函数是在全局作用域下调用的,因此它的指向就是全局对象Window,而全局对象中定义了a,因此打印出30 。
前面说过this在严格模式和非严格模式之间也会有一些差别,看下方代码:
function test() {
'use strict'
console.log(this)
}
test() // undefined
复制代码
可以看到在严格模式下,在全局作用域中调用test函数得到的是this并没有指向全局对象Window,而是undefined,这点是大家需要注意的。
隐式绑定规则
隐式绑定则是看调用的位置是否有上下文对象,或者说是否被某个对象调用,可以看下面代码:
function test() {
console.log(this.a)
}
var obj = {
a: 10,
test: test,
b: {
a: 20,
test1:test
}
}
var a = 30
obj.test() // 10
obj.b.test1() // 20
复制代码
可以看到obj.test()我们是使用obj对象来调用test函数,因此调用的位置就是obj,this就是指向了obj对象,打印出10。
obj.b.test1()中打印的是20,有可能有人会觉得test1函数的this指向的是obj对象,因为虽然是b对象调用了test1函数,但是b对象是obj里的一个属性,可以看成是obj调用了b对象再调用test1函数,因此this指向的是obj对象;但是实际并不是这样,因为this是会找离自己最近的调用位置,也就是b对象。
隐式丢失
function test() {
console.log(this.a)
}
function test1(fn) {
fn()
}
var obj = {
a: 10,
test: test,
}
var a = 30
const fn = obj.test // 函数别名
fn() // 30
test1(obj.test) //30
setTimeout( obj.test, 1000 ) //30
复制代码
fn 是obj.test的一个引用,但是实际上,他引用的是test函数本身,因此在全局作用域中调用fn()就可以看成在全局作用域中调用test函数。
test1(obj.test)将obj.test当成参数传入test1函数中调用,参数传递其实就是一种隐式赋值,本质上相当于fn = obj.test, 结果也就上面例子结果一样。
setTimeout( obj.test, 1000 )其实可以看成下面代码:
function setTimeout(fn, 1000) {
fn()
}
复制代码
这就相当与隐式赋值,也就与上面的例子是一样的。
显示绑定规则
显示绑定是利用apply()、call()、bind()等函数直接指定this的绑定对象、它们第一个参数就是this的指向,形式是xxx.call(...), 将xxx的this指向第一个参数,xxx是一个函数。
function test() {
console.log(this.a);
}
var obj = {
a: 20
}
var obj1 = {
a: 50
}
test.call(obj) // 20
test.apply(obj) // 20
var bd = test.bind(obj)
bd() // 20
var bd1 = bd.bind(obj1)
bd1() // 20
// bd1 = bd.bind(obj1) 等效于 bd1 = test.bind(obj).bind(onj1)
// bind的特性:只生效一次,就是第一个调时指向的this,后面的不会生效
复制代码
call和apply会立即执行函数、bind会返回一个新的函数。
解决绑定丢失的方法
可以使用显示绑定的一个变种->硬绑定来解决绑定丢失
function test() {
console.log(this.a);
}
var obj = {
a: 20,
test1: test
}
var a = 100
var test2 = function() {
test.call(obj)
}
test2() //20
setTimeout(test2, 100) //20
test2.call(window) //20
复制代码
在test2内部手动调用,test.call(obj),强制把test的this绑定到obj中,这就是一种硬绑定,可以看到经过硬绑定后打印出来的结果并不像隐式丢失this的例子打印全局的a;另外因为bind会返回一个新的函数,bind()也是一种硬绑定。
new绑定规则
在js中使用new会执行以下几步:
- 创建一个新的全新对象。
- 通过原型链将新对象与构造函数的原型链接。
- 将新对象的this指向构造函数。
- 如果函数没有返回其其他对象,那么new表达式中的含税调用会自动返回这个新对象。
function _new(constructorFn, ...args) {
const obj = {} // 创建一个新的全新对象。
obj.__proto__ = constructorFn.prototype // 通过原型链将新对象与构造函数的原型链接。
let res = constructorFn.apply(obj, args) // 将新对象的this指向构造函数,执行函数并返回结果
return res instanceof Object ? res : obj // 如果函数没有返回其其他对象,那么new表达式中的含税调用会自动返回这个新对象。
}
复制代码
可以看到在使用new操作符会将有this指向的改变。
function test(a) {
console.log(this.a);
}
var obj = new test(5)
obj.a //5
复制代码
this绑定的优先级
- 函数是否在new中调用,如果是this绑定新创建对象。
- 函数是否显示绑定,如果是this就绑定作为apply()、call()、bind()第一个参数的对象。
- 函数是否在某个上下文对象中调用(隐式绑定),如果是this绑定那个上下文对象。
- 如果都不是那就是默认绑定,this指向window,严格模式下指向undefin。
上面的顺序即为this绑定的优先级,new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
箭头函数
箭头函数是在ES6新增的,他相较于普通函数来说写法更加的简洁,下面来学习下箭头函数的一些特性。
var test = () => {
console.log(this.a);
}
var test1 = () => {
'use strict'
console.log(this);
}
var obj = {
a:20,
test:test
}
var a = 10
test() // 10
obj.test() //10
test.call(obj) //10
test.apply(obj) //10
var bd = test.bind(obj)
bd() //10
new test() //TypeError: test is not a constructor
test1() // Window
复制代码
从上面我的代码例子可以得出的结论:
- 箭头函数和普通函数不同,普通函数的this在调用的时候才绑定,箭头函数的this是声明时候就绑定了,它忽略任何形式的this改变,无论是使用隐式绑定、显示绑定或者new绑定,都无法修改箭头函数指向。
- 对箭头函数使用new操作符会报错,可以得出箭头函数不是一个构造器,不能当作构造函数。
- 箭头函数在严格模式下不是undefined,而是指向全局作用域Window。
箭头函数本身是没有自己的this,那么它的this是怎么来的呢,我们看下以下代码:
obj.test = function () {
var t1 = function () { // 普通函数
console.log(this, 't1'); // Window
var t2 = () => {
console.log(this, 't2'); // Window
}
t2()
}
t1()
}
obj.test()
obj.test1 = function () {
var t3 = () => { // 箭头函数
console.log(this, 't3'); // {a: 20, test: ƒ, test1: ƒ}
var t4 = () => {
console.log(this, 't4'); // // {a: 20, test: ƒ, test1: ƒ}
}
t4()
}
t3()
}
obj.test1()
复制代码
首先我们给出结论this指向的结论:箭头函数的this指向外层作用域的this;下面再来解析上面的代码。
可以看看两个函数的对比,第一个函数中t1是一个普通函数,第二个函数中t3是一个箭头函数,这就是两个函数的不同之处。
第一个函数中t1函数虽然是在obj.test函数中调用的,但是它是一个默认绑定,所以this指向Window,t2是在t1函数中调用的,t2是一个箭头函数本身并没有this,因此它会指向外层作用域的this,也就是t1的this->Window。
第二个函数t3是一个箭头函数,它自身就没有this,因此指向外层obj对象,而t4呢也是一个箭头函数也没有this,那么它也会往外找this,它的外层是t3,但是t3也是箭头函数自身并没有this,因此t4会再往上找,找到obj对象,将this绑定到obj对象中。
总结
this的指向就的学习就分享这么多了,现实无论是面试中还是工作当中遇到this的问题,首先看是普通函数还是箭头函数,当前的环境是否是严格模式下,然后普通函数的调用符合哪条绑定规则,如果箭头函数就是指向有this的外层作用域。
本文参考的书籍
- 《你不知道的JavaScript书籍》