js中的call

2021-02-08  本文已影响0人  攻城熊

java中对象包含属性和方法, 方法即函数, 只有对象可以调用方法
但是js中一切皆对象, 函数是对象, 函数也可以调用方法
Function.prototype.call
Function.prototype.apply
Function.prototype.bind
Function.prototype.toString
为什么js语言会在function原型上挂上这四个函数?
因为好处太多了, 现在我就说一说call的应用

// A对象
let a = {
    name: "aaa",
    toName(age) {
        console.log(age, "我的名字:", this.name)
    }
}
// B对象
let b = {
    name: "bbb",
}
// A对象调用自己的方法
a.toName('A对象')
// A对象具有某种能力, 能打印自己的名字,但是如何与别人共享这种能力呢?
// 编程思想促进了语法的形成 ==>  于是js语法就设计了这种通用能力
a.toName.call(b, 'B对象')

再深入思考一下, call语法是基于什么形成的呢 ?
了解底层语法有助于我们理解高级的语法
因为在底层的语法眼中, 高级语法只不过是工具, 根本不算语法, 不过是由我定义构建处理来的

理解call函数, 核心必要知识

  1. 接受函数也可以调用函数这个观念(违反直觉, 需要编程思维, 原型 原型链 prototype)
  2. 词法作用域 作用域链(不同浏览器内核不同, 谷歌浏览器是v8设计出来的js执行机制, 底层整个系统基于算法 + 数据结构, 总之, 这就是js执行规则的制定者)
  3. 谁是this, 即谁在调用函数
    函数调用, 需要知道函数当前的上下文信息,
    因为函数内部的作用域可能会引用函数拥有者的相关信息, 函数拥有者就是函数的上下文,
    找不到需要的信息, 通过作用域链就会在全局作用域寻找
Object.prototype.mycall = function(obj){
    // a.toName.mycall(b, 'B对象')
    // 拆开分析 , 上面是我们的目标
    // 1. 可以看出, a.toName就是我们要写的这个函数的调用者, 'B对象',就是传入的第二个参数
    let fn = this 
    // 此时fn就是我们要借用的函数 a.toName 用来打印名字, 但是此时它打印的名字依然是A对象的
    // 2. 如何让它打印传入对象obj, 也就是b的名字呢?
    // 作用域分析, 方法一: 将A对象与B对象交换作用域 (v8也许有,但我不知道)
    // 方法二: 打不过就加入, A对象抄袭B对象, 将函数复制一份, 变成自己的函数, 然后调用自己的函数
    obj.copyFn = fn
    obj.copyFn()
}

call的本质并不是调用别人的函数, 而是copy了别人的函数

哈哈哈, v8内部的不知道, 反正js基于现有语法就有这个能力 (还有解法, 基于proto,原型链的方式, var temp = Object.create(null)), 利用干净的地方, 防止覆盖原对象的函数

Function.prototype.call = function(ctx, ...args) {
    var proto = null
    if ('__proto__' in ctx) {
        proto = ctx.__proto__
    }
    var temp = Object.create(proto)
    temp.$f = this
    ctx.__proto__ = temp
    var result = ctx.$f(...args)
    ctx.__proto__ = proto
    return result
}

显然, 我们自己的上面的mycall还要很多问题

  1. 将传入的对象添加了copyFn函数, 可能重置掉对象原有的copyFn, 修改了对象, 需要delete删除
  2. 传入对象需要进行类型判断
  3. 传入参数需要处理
  4. 返回值
Object.prototype.mycall = function (obj) {
    obj = obj || window   //node环境下无法使用window 
    // 这里如果害怕属性覆盖可以设置唯一值, 这里就不展开了
   // Object.getOwnPropertyNames()  // 获得对象的自己的属性[]
    let fn = this
    //给context添加一个方法 指向this
    obj[fn] = this
    // 处理参数 去除第一个参数this 其它传入fn函数
    let arg = [...arguments].slice(1)
    // 或者使用rest运算符获取剩余参数
    // function f(a, ...arg) {
    //     console.log(arg, Array.isArray(arg))
    // }
    // f()         //[] true
    // f(1)       //[] true
    // f(1, 2)   //[2] true
    // f(1, 2, 3)  //[2, 3] true
    let res = context[fn](...arg) //执行fn
    delete context[fn] //删除属性
    return res
}

这里我们就实现了自己的call, 以后看见call就知道都不过是些雕虫小技了(当然真实的实现方式肯定需要考虑更多因素和性能, 而且在v8更大的环境下, 可能还不止一种实现方式,保持谦虚)

call的雕虫小技

  1. Array, String等对象函数的借调
    Array.prototype.slice.call(arguments)
    [].slice.call(arguments)
    Object.prototype.toString.call(arr).slice(8, -1) // Array

  2. 实现借用构造函数继承

    function  Person(name,age,love){ 
        this.name=name; 
        this.age=age; 
        this.love=love; 
        this.say=function say(){ 
            alert("姓名:"+name); 
        } 
    } 
    //call方式 
    function student(name,age){ 
        Person.call(this,name,age); 
    } 
  1. vue源码中也有相关应用先写这么多了哈.
上一篇下一篇

猜你喜欢

热点阅读