面试复习记-this

166 阅读8分钟

重温面试常客,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会执行以下几步:

  1. 创建一个新的全新对象。
  2. 通过原型链将新对象与构造函数的原型链接。
  3. 将新对象的this指向构造函数。
  4. 如果函数没有返回其其他对象,那么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绑定的优先级

  1. 函数是否在new中调用,如果是this绑定新创建对象。
  2. 函数是否显示绑定,如果是this就绑定作为apply()、call()、bind()第一个参数的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定),如果是this绑定那个上下文对象。
  4. 如果都不是那就是默认绑定,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 
复制代码

从上面我的代码例子可以得出的结论:

  1. 箭头函数和普通函数不同,普通函数的this在调用的时候才绑定,箭头函数的this是声明时候就绑定了,它忽略任何形式的this改变,无论是使用隐式绑定、显示绑定或者new绑定,都无法修改箭头函数指向。
  2. 对箭头函数使用new操作符会报错,可以得出箭头函数不是一个构造器,不能当作构造函数。
  3. 箭头函数在严格模式下不是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书籍》