一网打尽this,对执行上下文说Yes

392 阅读6分钟

一网打尽this,对执行上下文说Yes

this 到底指向谁?

  • 说到这里,还得回归JS的基本概念-执行上下文 这个概念之后会进行扩展

这里可以先死记硬背几条规律

  • 在函数体中,非显式或者隐式简单调用函数时,
    • 严格模式下,会被绑定到undefined上面
    • 非严格模式,会被绑定到全局对象window
  • 一般使用new方法调用构造函数,内部的this会被绑定到新创建的对象
  • 一般使用call/apply/bind方法调用函数,会指向传进去的参数对象
  • 一般使用上下文对象调用函数,this会绑定到上下文对象
  • 在箭头函数中,this指向是由外层(函数或者全局作用域)决定的

1 全局中的this

function f1():void{
    console.log(this)
}

function f2():void{
    'use strict'
    console.log(this)
}
f1() // window
f2() // undefined

这道题相对简单,严格模式下,会被绑定到undefined上面,非严格模式,会被绑定到全局对象window

1.1 但是需要注意变式

const foo = {
    bar: 10,
    fn: function() {
        console.log(this)
        console.log(this.bar)
    }
}
var fn1 = foo.fn
fn1()
这里的this依然指向`window`,虽然fn函数是作为foo方法调用的,但是在赋值给fn1后,fn1的调用依然是在全局对象中进行的,因此会输出`window``undefined`
  • 等价于以下代码
console.log(window)
console.log(window.bar) // 去window 上找bar,那可不就undefined了嘛

1.2 变式2

如果改成以下形式

const foo = {
    bar: 10
    fn: function(){
        console.log(this)
        console.log(this.bar)
    }
}
foo.fn()
  • 输出将会是
{bar: 10,fn: f}
10

这时,this指向的是最后调用他的对象,

法则1:在执行时候不考虑显式绑定,如果函数中的this是被上一级调用的就是此对象,否则就是全局环境

2 上下文中的this

2.1 普通上下文对象

const student = {
    name: 'lucas',
    fn: function(){
        return this.name
    }
}
console.log(student.fn() === 'lucas') // true

2.2 this指向最后调用的对象

const person = {
    name: 'lucas',
    brother: {
        name: 'Mike',
        fn: function() {
            return this.name
        }
    }
}
console.log(person.brother.fn()) // Mike 

2.3 是不是很简单?(进阶)

const o1 = {
    text: 'o1';
    fn: function() {
        return this.text
    } 
}
const o2 = {
    text: 'o2';
    fn: function() {
        return o1.fn()
    } 
}
const o3 = {
    text: 'o3';
    fn: function() {
        var fn = o1.fn
        return fn()
    } 
}
  • 第一个很简单是指向上下文,输出o1
  • 第二个也很简单去调用这个o1.fn(),还是o1
  • 第三个也很简单,由于变量提升,以上代码会转化成如下代码
const o3 = {
    text: 'o3';
    fn: function() {
        var fn; // undefined
        fn = o1.fn;
        return fn()
    } 
}
console.log(o3.fn())
  • 相当于var fn = o1.fn;从右向左o1.fn的fn方法赋值给fn.
  • 此时,fn = o1.fn等价fn = fn() ,右边的fno1的方法
  • 裸调用一个方法,fn() 等价于window.fn()
  • 此时,相当于o3.window.fn()当然是undefined

答案是 o1 o1 undefined

2.4 简单吗?再进阶一下

如果此时这面试题你全部写对,面试官会问,那怎么让第二个输出02呢?

  • 你会想到那还不简单?
    • bind,call,apply
  • 但是interviewer会继续问,除了这些呢?

这个问题就考察思考问题的深度和随机应变了

const o1 = {
    text: 'o1',
    fn: function() {
        return this.text
    }
}
const o2 = {
    text: 'o2',
    fn: o1.fn
}
console.log(o2.fn())
  • 刚才已经说到,this指向最后调用他的对象,在上面代码我提前将o1fn方法通过赋值操作,挂载到o2fn属性上
// 相当于这样
const o2 = {
    text: 'o2',
    fn: function() {
        return this.text
    } // 这个function 是原本o1的方法
}

3 bind,call,apply

3.1 区别

这个问题比较基础

  • bind和apply,call的联系是都可以显式重定 this 指向到传递进去的参数对象
  • 区别是:
    • bind 会创建一个新的函数,不执行之前的函数
    • apply call 第一个参数都是需要重定向的对象,前者需要其他参数传递一个数组,call则以字符串传递

一句话总结就是 apply 和 call,bind都会改变相关函数的this指向,call apply 会执行原函数,bind不会执行原函数,而是返回新的函数,这个新的函数已经自定绑定了新的this指向,并且可以手动调用它

再具体一点就是call apply区别在参数设定,上面已经说过了

  • 用代码来总结的话,以下的3个代码都是等价的
// 1
const target = {}
fn.call(target,'arg1','arg2')
// 2 
const target = {}
fn.apply(target,['arg1','arg2'])
// 3 
const target = {}
fn.bind(target,'arg1','arg2')
const fo = {
	name: 'lucas',
	logName: function():void {
        console.log(this.name)
    }
}
const bar = {
    name: 'mike'
}
console.log(fo.logName.call(bar)) // 由 lucas => mike

本身不难理解,但是一般会结合继承来实现,之后单独写文讲

4 构造函数中的this

4.1 这个直接看题就行

function Fo() {
    this.bar = 'lucas';
}
const instance = new Fo();
console.log(instance.bar) // 'lucas'

可能 interviewer 会接着问 new 操作符,提供简略班回答

  • 创建了新对象
  • 构造函数的this指向新对象
  • 可以添加属性方法
  • 并返回这个新的对象
const obj = {} 
obj.__proto__ = Foo.prototype;
Foo.call(obj)

新对象 => 构造函数的原型对象给 新对象的原型 => 构造函数通过call绑定到新对象

当然这里是简单版本,后续会手动实现new bind apply call 等,后续再说

4.2 如果构造函数中有return的情况

function Foo() {
    this.user = 'lucas';
    const p = {};
    return p
} 
const i = new Foo()
i.user// this => {}
function Foo() {
    this.user = 'lucas';
    const p = {};
    return 1
}
const i = new Foo()
i.user// this => 1
  • 相信大家猜到了,return 复杂类型就是这个对象,如果是简单类型就是new操作符号的实例

5 箭头函数

先说结论,(哎呦不错哦),不好意思说错了

  • 外层(函数或者全局作用域决定) 书中是this: the enclosing(function or global) scope
  • setTimeout
const foo = {
	fn() {
		setTimeout(function(){
            clg(this)
        },1000)
	}
}
clg(foo,fn()) // 匿名函数的this => window
const foo = {
	fn () {
		setTimeout(()=>{
            clg(this)
        },1000)
	}
}
clg(foo,fn()) // {fn,f}

5.2 this的优先级

箭头函数的没有难度,一般会配合this优先级

  • call apply bind new => 显式
  • 根据调用关系 => 隐式
function foo (a) {
    clg(this.a)
}

const obj1 = {
    a:1,
    foo:foo
}
const obj2 = {
    a:2,
    foo:foo
}

obj1.foo.call(obj2) // 2
obj2.foo.call(obj1) // 1
  • 显式 > 隐式

bind 和 new

function foo(a) {
    this.a = a
}

const obj1 = {};

var bar = foo.bind(obj1);
bar(2)

clg(obj1.a) // {a: 2}

bind 将 bar函数中的this绑定到obj1上,bar(2)执行后,obj.a = 2

  • 再使用构造函数
const bar = new Bar(3)
clg(bar.a) // 3

bar本身是通过bind 构造函数,内部已经将this绑定为obj1,再次作为构造函数new调用时候,返回的实例就和obj1解绑了,也就说,new 绑定了修改bind

new > bind

function foo () {
    return a => {
        clg(this.a)
    }
}

const obj1 = {
    a: 2
}
const obj2 = {
    a: 3
}

const bar = foo.call(obj1)
clg(bar.call(obj2)) // 2
// 由于foo 中的this 绑定到了obj1上,所以 bar 中的this 会绑定到obj1,箭头函数的绑定无法修改

说到这里,开放的立例题分析:

实现一个面试经典的bind函数,有兴趣的可以做一下,下次更新闭包和补充this吧,还有小部分可以写,bye