资讯专栏INFORMATION COLUMN

zr_hebo / 2595人阅读

彻底弄清 this call apply bind 以及原生实现

有关 JS 中的 this、call、apply 和 bind 的概念网络上已经有很多文章讲解了 这篇文章目的是梳理一下这几个概念的知识点以及阐述如何用原生 JS 去实现这几个功能

this 指向问题 this

this 的指向在严格模式和非严格模式下有所不同;this 究竟指向什么是,在绝大多数情况下取决于函数如何被调用

全局执行环境的情况:

非严格模式下,this 在全局执行环境中指向全局对象(window、global、self);严格模式下则为 undefined

作为对象方法的调用情况:

假设函数作为一个方法被定义在对象中,那么 this 指向最后调用他的这个对象

比如:

a = 10
obj = {
    a: 1,
    f() {
        console.log(this.a) // this -> obj
    }
}

obj.f() // 1 最后由 obj 调用

obj.f() 等同于 window.obj.f() 最后由 obj 对象调用,因此 this 指向这个 obj

即便是这个对象的方法被赋值给一个变量并执行也是如此:

const fn = obj.f
fn() // 相当于 window.fn() 因此 this 仍然指向最后调用他的对象 window

call apply bind 的情况:

想要修改 this 指向的时候,我们通常使用上述方法改变 this 的指向

a = 10
obj = {
    a: 1
}
function fn(...args) {
    console.log(this.a, "args length: ", args)
}

fn.call(obj, 1, 2)
fn.apply(obj, [1, 2])
fn.bind(obj, ...[1, 2])()

可以看到 this 全部被绑定在了 obj 对象上,打印的 this.a 也都为 1

new 操作符的情况:

new 操作符原理实际上就是创建了一个新的实例,被 new 的函数被称为构造函数,构造函数 new 出来的对象方法中的 this 永远指向这个新的对象:

a = 10
function fn(a) { this.a = a }
b = new fn(1)
b.a // 1

箭头函数的情况:

普通函数在运行时才会确定 this 的指向

箭头函数则是在函数定义的时候就确定了 this 的指向,此时的 this 指向外层的作用域

a = 10
fn = () => { console.log(this.a) }
obj = { a: 20 }
obj.fn = fn
obj.fn()
window.obj.fn()
f = obj.fn
f()

无论如何调用 fn 函数内的 this 永远被固定在了这个外层的作用域(上述例子中的 window 对象)

this 改变指向问题

如果需要改变 this 的指向,有以下几种方法:

箭头函数

内部缓存 this

apply 方法

call 方法

bind 方法

new 操作符

箭头函数

普通函数

a = 10
obj = {
    a: 1,
    f() { // this -> obj
        function g() { // this -> window
            console.log(this.a)
        }
        g()
    }
}

obj.f() // 10

在 f 函数体内 g 函数所在的作用域中 this 的指向是 obj:

在 g 函数体内,this 则变成了 window:

改为箭头函数

a = 10
obj = {
    a: 1,
    f() { // this -> obj
        const g = () => { // this -> obj
            console.log(this.a)
        }
        g()
    }
}

obj.f() // 1

在 f 函数体内 this 指向的是 obj:

在 g 函数体内 this 指向仍然是 obj:

内部缓存 this

这个方法曾经经常用,即手动缓存 this 给一个名为 _thisthat 等其他变量,当需要使用时用后者代替

a = 10
obj = {
    a: 20,
    f() {
        const _this = this
        setTimeout(function() {
            console.log(_this.a, this.a)
        }, 0)
    }
}

obj.f() // _this.a 指向 20 this.a 则指向 10

查看一下 this 和 _this 的指向,前者指向 window 后者则指向 obj 对象:

call

call 方法第一个参数为指定需要绑定的 this 对象;其他参数则为传递的值:

需要注意的是,第一个参数如果是:

null、undefined、不传,this 将会指向全局对象(非严格模式下)

原始值将被转为对应的包装对象,如 f.call(1) this 将指向 Number,并且这个 Number 的 [[PrimitiveValue]] 值为 1

obj = {
    name: "obj name"
}

{(function() {
    console.log(this.name)
}).call(obj)}

apply

与 call 类似但第二个参数必须为数组:

obj = {
    name: "obj name"
}

{(function (...args){
    console.log(this.name, [...args])
}).apply(obj, [1, 2, 3])}

bind

比如常见的函数内包含一个异步方法:

function foo() {
    let _this = this // _this -> obj
    setTimeout(function() {
        console.log(_this.a) // _this.a -> obj.a
    }, 0)
}
obj = {
    a: 1
}
foo.call(obj) // this -> obj
// 1

我们上面提到了可以使用缓存 this 的方法来固定 this 指向,那么使用 bind 代码看起来更加优雅:

function foo() { // this -> obj
    setTimeout(function () { // 如果不使用箭头函数,则需要用 bind 方法绑定 this
        console.log(this.a) // this.a -> obj.a
    }.bind(this), 100)
}
obj = {
    a: 1
}

foo.call(obj) // this -> obj
// 1

或者直接用箭头函数:

function foo() { // this -> obj
    setTimeout(() => { // 箭头函数没有 this 继承外部作用域的 this
        console.log(this.a) // this.a -> obj.a
    }, 100)
}
obj = {
    a: 1
}

foo.call(obj) // this -> obj
// 1

new 操作符

new 操作符实际上就是生成一个新的对象,这个对象就是原来对象的实例。因为箭头函数没有 this 所以函数不能作为构造函数,构造函数通过 new 操作符改变了 this 的指向。

function Person(name) {
    this.name = name // this -> new 生成的实例
}
p = new Person("oli")
console.table(p)

this.name 表明了新创建的实例拥有一个 name 属性;当调用 new 操作符的时候,构造函数中的 this 就绑定在了实例对象上

原生实现 call apply bind new

文章上半部分讲解了 this 的指向以及如何使用 call bind apply 方法修改 this 指向;文章下半部分我们用 JS 去自己实现这三种方法

myCall

首先 myCall 需要被定义在 Function.prototype 上这样才能在函数上调用到自定义的 myCall 方法

然后定义 myCall 方法,该方法内部 this 指向的就是 myCall 方法被调用的那个函数

其次 myCall 第一个参数对象中新增 this 指向的这个方法,并调用这个方法

最后删除这个临时的方法即可

代码实现:

Function.prototype.myCall = function(ctx) {
    ctx.fn = this
    ctx.fn()
    delete ctx.fn
}

最基本的 myCall 就实现了,ctx 代表的是需要绑定的对象,但这里有几个问题,如果 ctx 对象本身就拥有一个 fn 属性或方法就会导致冲突。为了解决这个问题,我们需要修改代码使用 Symbol 来避免属性的冲突:

Function.prototype.myCall = function(ctx) {
    const fn = Symbol("fn") // 使用 Symbol 避免属性名冲突
    ctx[fn] = this
    ctx[fn]()
    delete ctx[fn]
}
obj = { fn: "functionName" }
function foo() { console.log(this.fn) }

foo.myCall(obj)

同样的,我们还要解决参数传递的问题,上述代码中没有引入其他参数还要继续修改:

Function.prototype.myCall = function(ctx, ...argv) {
    const fn = Symbol("fn")
    ctx[fn] = this
    ctx[fn](...argv) // 传入参数
    delete ctx[fn]
}
obj = { fn: "functionName", a: 10 }
function foo(name) { console.log(this[name]) }

foo.myCall(obj, "fn")

另外,我们还要检测传入的第一个值是否为对象:

Function.prototype.myCall = function(ctx, ...argv) {
    ctx = typeof ctx === "object" ? ctx || window : {} // 当 ctx 是对象的时候默认设置为 ctx;如果为 null 则设置为 window 否则为空对象
    const fn = Symbol("fn")
    ctx[fn] = this
    ctx[fn](...argv)
    delete ctx[fn]
}
obj = { fn: "functionName", a: 10 }
function foo(name) { console.log(this[name]) }

foo.myCall(null, "a")

如果 ctx 为对象,那么检查 ctx 是否为 null 是则返回默认的 window 否则返回这个 ctx 对象;如果 ctx 不为对象那么将 ctx 设置为空对象(按照语法规则,需要将原始类型转化,为了简单说明原理这里就不考虑了)

执行效果如下:

这么一来自定义的 myCall 也就完成了

另外修改一下检测 ctx 是否为对象可以直接使用 Object;delete 对象的属性也可改为 ES6 的 Reflect:

Function.prototype.myCall = function(ctx, ...argv) {
    ctx = ctx ? Object(ctx) : window
    const fn = Symbol("fn")
    ctx[fn] = this
    ctx[fn](...argv)
    Reflect.deleteProperty(ctx, fn) // 等同于 delete 操作符
    return result
}
myApply

apply 效果跟 call 类似,将传入的数组通过扩展操作符传入函数即可

Function.prototype.myApply = function(ctx, argv) {
    ctx = ctx ? Object(ctx) : window
    // 或者可以鉴别一下 argv 是不是数组
    const fn = Symbol("fn")
    ctx[fn] = this
    ctx[fn](...argv)
    Reflect.deleteProperty(ctx, fn) // 等同于 delete 操作符
    return result
}
myBind

bind 与 call 和 apply 不同的是,他不会立即调用这个函数,而是返回一个新的 this 改变后的函数。根据这一特点我们写一个自定义的 myBind:

Function.prototype.myBind = function(ctx) {
    return () => { // 要用箭头函数,否则 this 指向错误
        return this.call(ctx)
    }
}

这里需要注意的是,this 的指向原因需要在返回一个箭头函数,箭头函数内部的 this 指向来自外部

然后考虑合并接收到的参数,因为 bind 可能有如下写法:

f.bind(obj, 2)(2)
// or
f.bind(obj)(2, 2)

修改代码:

Function.prototype.myBind = function(ctx, ...argv1) {
    return (...argv2) => {
        return this.call(ctx, ...argv1, ...argv2)
    }
}

另外补充一点,bind 后的函数还有可能会被使用 new 操作符创建对象。因此 this 理应被忽略但传入的参数却正常传入。

举个例子:

obj = {
    name: "inner" // 首先定义一个包含 name 属性的对象
}
function foo(fname, lname) { // 然后定义一个函数
    this.fname = fname
    console.log(fname, this.name, lname) // 打印 name 属性
}
foo.prototype.age = 12

然后我们使用 bind 创建一个新的函数并用 new 调用返回新的对象:

boundf = foo.bind(obj, "oli", "young")
newObj = new boundf()

看图片得知,尽管我们定义了 obj.name 并且使用了 bind 方法绑定 this 但因使用了 new 操作符 this 被重新绑定在了 newObj 上。因此打印出来的 this.name 就是 undefined 了

因此我们还要继续修改我们的 myBind 方法:

Function.prototype.myBind = function (ctx, ...argv1) {
    let _this = this
    let boundFunc = function (...argv2) { // 这里不能写成箭头函数了,因为要使用 new 操作符会报错
        return _this.call(this instanceof boundFunc ? this : ctx, ...argv1, ...argv2) // 检查 this 是否为 boundFunc 的实例
    }
    return boundFunc
}

然后我们使用看看效果如何:

this 指向问题解决了但 newObj 实例并未继承到绑定函数原型中的值,因此还要解决这个问题,那么我们直接修改代码增加一个 prototype 的连接:

Function.prototype.myBind = function (ctx, ...argv1) {
    let _this = this
    let boundFunc = function (...argv2) {
        return _this.call(this instanceof boundFunc ? this : ctx, ...argv1, ...argv2)
    }
    boundFunc.prototype = this.prototype // 连接 prototype 继承原型中的值
    return boundFunc
}

看起来不错,但还是有一个问题,尝试修改 boundf 的原型:

发现我们的 foo 中原型的值也被修改了,因为直接使用 = 操作符赋值,其实本质上还是原型的值,最后我们再修改一下,使用一个空的函数来重新 new 一个:

Function.prototype.myBind = function (ctx, ...argv1) {
    let _this = this
    let temp = function() {} // 定义一个空的函数
    let boundFunc = function (...argv2) {
        return _this.call(this instanceof temp ? this : ctx, ...argv1, ...argv2)
    }
    temp.prototype = this.prototype // 继承绑定函数原型的值
    boundFunc.prototype = new temp() // 使用 new 操作符创建实例并赋值
    return boundFunc
}

最后看下效果:

new 操作符

最后我们再来实现一个 new 操作符名为 myNew

new 操作符的原理是啥:

生成新的对象

绑定 prototype (既然是 new 一个实例,那么实例的 __proto__ 必然要与构造函数的 prototype 相连接)

绑定 this

返回这个新对象

代码实现:

function myNew(Constructor) { // 接收一个 Constructor 构造函数
    let newObj = {} // 创建一个新的对象
    newObj.__proto__ = Constructor.prototype // 绑定对象的 __proto__ 到构造函数的 prototype
    Constructor.call(newObj) // 修改 this 指向
    return newObj // 返回这个对象
}

然后考虑传入参数问题,继续修改代码:

function myNew(Constructor, ...argv) { // 接收参数
    let newObj = {}
    newObj.__proto__ = Constructor.prototype
    Constructor.call(newObj, ...argv) // 传入参数
    return newObj
}

小结

到此为止

this 指向问题

如何修改 this

如何使用原生 JS 实现 call apply bind 和 new 方法

再遇到类似问题,基本常见的情况都能应付得来了

(完)

参考:

https://juejin.im/post/59bfe8...

https://segmentfault.com/a/11...

https://github.com/Abiel1024/...

感谢 webgzh907247189 修改了一些代码实现

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108985.html

相关文章

  • vue中如何实现的自定义按钮

    摘要:在实际开发项目中,有时我们会用到自定义按钮因为一个项目中,众多的页面,为了统一风格,我们会重复用到很多相同或相似的按钮,这时候,自定义按钮组件就派上了大用场,我们把定义好的按钮组件导出,在全局引用,就可以在其他组件随意使用啦,这样可以大幅度 在实际开发项目中,有时我们会用到自定义按钮;因为一个项目中,众多的页面,为了统一风格,我们会重复用到很多相同或相似的按钮,这时候,自定义按钮组件就...

    biaoxiaoduan 评论0 收藏0
  • JavaScript代码整洁之道

    摘要:代码整洁之道整洁的代码不仅仅是让人看起来舒服,更重要的是遵循一些规范能够让你的代码更容易维护,同时降低几率。另外这不是强制的代码规范,就像原文中说的,。里式替换原则父类和子类应该可以被交换使用而不会出错。注释好的代码是自解释的。 JavaScript代码整洁之道 整洁的代码不仅仅是让人看起来舒服,更重要的是遵循一些规范能够让你的代码更容易维护,同时降低bug几率。 原文clean-c...

    liaorio 评论0 收藏0
  • 前端经典面试题总结

    摘要:接着我之前写的一篇有关前端面试题的总结,分享几道比较经典的题目第一题考点作用域,运算符栗子都会进行运算,但是最后之后输出最后一个也就是那么其实就是而且是个匿名函数,也就是属于,就输出第二和第三个都是类似的,而且作用域是都是输出最后一个其实就 接着我之前写的一篇有关前端面试题的总结,分享几道比较经典的题目: 第一题: showImg(https://segmentfault.com/im...

    BlackMass 评论0 收藏0
  • 私有云那家好-六大私有云厂商详细对比!

    对比内容UCloudStackZStackVMwareQingCloud腾讯TStack华为云Stack优势总结•基于公有云自主可控•公有云架构私有化部署•轻量化/轻运维/易用性好•政府行业可复制案例轻量化 IaaS 虚拟化平台•轻量化、产品成熟度高•业内好评度高•功能丰富、交付部署快•中小企业案例多全套虚拟产品及云平台产品•完整生态链、技术成熟•比较全面且健全的渠道•产品成熟度被市场认可,市场占...

    ernest.wang 评论0 收藏0
  • cross-env使用记录

    摘要:能跨平台地设置及使用环境变量让这一切变得简单,不同平台使用唯一指令,无需担心跨平台问题安装方式改写使用了环境变量的常见如在脚本多是里这么配置运行,这样便设置成功,无需担心跨平台问题关于跨平台兼容,有几点注意 cross-env能跨平台地设置及使用环境变量, cross-env让这一切变得简单,不同平台使用唯一指令,无需担心跨平台问题 1、npm安装方式 npm i --save-de...

    Michael_Ding 评论0 收藏0
  • webpack打包插件

    摘要:引入的模块引入的使用将打包打包的拆分将一部分抽离出来物理地址拼接优化打包速度压缩代码,这里使用的是,同样在的里面添加 const path = require(path); //引入node的path模块const webpack = require(webpack); //引入的webpack,使用lodashconst HtmlWebpackPlugin = require(ht...

    ChanceWong 评论0 收藏0

发表评论

0条评论

zr_hebo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<