This?You know?

97 阅读5分钟

「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战

写在前面

在前端领域,this对我来说曾经像梦一样,猜不透,摸不着。

相信有不少小伙伴们对于this也有过我曾经的处境,本文将带领大家一起彻底搞懂this

在开始之前,我们需要先消除一些对this的误解,人们很容易把this理解成指向函数自身。

我们来看一个栗子🌰

function foo() {
    console.log(this.count)
}

foo.count = 1

window.count = 2

foo() // 2

console.log(foo.count) // 1

按照上面的逻辑,foo输出的应该是1但是输出的2,这明显是与上面的想法矛盾的,那么具体为什么呢,我们慢慢来看。

what is this

区别于其他语言,js中的this总是会指向一个对象,而这个对象在运行时由函数的执行环境动态确定,并非函数被声明时的环境。

去除witheval这两种不常用的存在欺骗作用域的的用法,this一般有四种指向:

  • 作为对象的方法调用(隐式绑定) -----> this指向该对象

image.png

  • 作为普通函数调用(默认绑定) -----> this指向window

image.png

ES5strict模式下,这种情况下的this已经被规定为不会指向全局对象,而是undefined

image.png

  • 构造器调用(new绑定) -----> this指向实例对象

image.png

  • call/apply/bind(显示绑定) -----> this指向传递的对象

image.png

跟普通的函数调用相比,call/apply/bind可以动态地改变函数的this,我们会在接下来的篇幅中更详细的介绍他们。

where is this

我们再看一段代码

window.name = 'Varlet'
var joker = {
    name: 'Jokerrr',
    getName(){
        console.log(this.name)
    }
}
joker.getName() // Jokerrr
const fun = joker.getName
fun()  // Varlet

我们来分析一下

  • 在执行joker.getNamegetName作为joker对象的属性被调用,此时的this指向joker,所以输出Jokerrr

  • 当我们用fun来引用joker.getName并调用时,为普通函数调用模式,此时this指向window

call apply bind

先说说callapply,他们的作用是一模一样,如果要说区别,估计只有参数的传入方式不同。

apply

apply接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组。

apply把这个集合中的元素作为参数传给被调用的函数。

举个栗子🌰

function fun(a,b,c){
    console.log(a,b,c)
}
fun.apply(null,[1,2,3]) // 1 2 3

在上面的代码中,我们将1,2,3放在一个数组中一起传入到fun中,他们也分别对应funa,b,c三个参数。

call

call传入的参数的数量是不固定的,但是跟apply相同的是,第一个参数也是代表函数体内的this指向。从第二个参数开始,每个参数被依次传入函数。

function fun(a,b,c){
    console.log(a,b,c)
}
fun.call(null,1,2,3) // 1,2,3

其实,js函数接收的参数在内部也是以类数组的形式表示的。

function fun(){
    console.log(arguments)
}
fun.call(null,1,2,3)

image.png

以上我们可以看出applycall的使用率更高,使用apply的话我们可以不去关注到底传入多少个参数,直接塞数组里推过去就行了。

call其实是apply的一个语法糖,如果我们明确的知道传入参数而且想一目了然的看清楚形参和实参的对照关系,也可以使用call 当使用call或者apply的时候,如果我们传入的第一个参数为null,函数体内的this会指 向默认的宿主对象,在浏览器中则是window

bind

用来指定函数内部this指向,区别于applycall的是,bind后函数不会立即执行而是返回一个新的函数,而另外两个则会直接执行返回执行结果。

function fun(){
    console.log(this)
}
let a = {name:'a'}

console.log(fun.bind(a))

image.png

需要注意的是,bind参数传递的方式与call一致。

实现 call apply bind new

先看看实现call apply的主要思路

  • 第一个参数为undefinednull的时候,将this指向window
  • 改变了this指向,让新的对象执行该函数。

手写apply

Function.prototype.myApply = function(context){
    if(typeof context === 'undefined' || context === null){
        context = window
    }
    context.fn = this
    const args = arguments[1]||[]
    const result =context.fn(...args)
    delete context.fn
    return result
}

看看效果

image.png

手写call

Function.prototype.myCall = function(context){
    if(typeof context === 'undefined' || context === null){
        context = window
    }
    context.fn = this
    const args = [...arguments].slice(1)
    const result = context.fn(...args)
    delete context.fn
    return result
}

看看效果

image.png

手写new

new的流程

  • 创建一个空的js对象
  • 必须为一个函数且不能为箭头函数
  • 设置新对象的__proto__为构造函数的prototype
  • 使用新的对象执行函数,函数的上下文为新的对象
  • 如果函数设置了返回值并且返回值是一个对象的话返回该对象,否则返回新创建的js对象
function myNew(fun){
    if(typeof fun !== "function" || !fun.prototype) throw Error(`fun is not a constructor`);
    
    const res = Object.create(fun.prototype);
    
    const result = fun.apply(res,[...arguments].slice(1))
    if(
        (typeof result === "object" || typeof result === "function") 
        &&typeof result !== null
       ){ 
        return result 
    } 
    return res 
}

看看效果

image.png

手写bind

  • bind方法不会立即执行,需要返回一个待执行的函数
  • 实现作用域绑定
  • 参数传递
Function.prototype.myBind = function () {
    let that = this
    const args = [...arguments]
    return function () {
        that.call(...args)
    }
}

看看效果

image.png

最后

以上,就是本人对于this的一些浅显的理解,希望能对你有所帮助。

同时向大家推荐本人参与的vue3移动端开源组件库 varlet 欢迎大家 star && pr