资讯专栏INFORMATION COLUMN

日积跬步,apply/call/bind 自我实现

denson / 3225人阅读

摘要:日常编码中被开发者用来实现对象冒充,也即显示绑定。面试题源码实现,事实上是对基础知识的一个综合考核。原型链终端指向,不会有构造函数,也不会有等属性,这些属性来自。

call/apply/bind 日常编码中被开发者用来实现 “对象冒充”,也即 “显示绑定 this“。

面试题:“call/apply/bind源码实现”,事实上是对 JavaScript 基础知识的一个综合考核。

相关知识点:

作用域;

this 指向;

函数柯里化;

原型与原型链;

call/apply/bind 的区别

三者都可用于显示绑定 this;

call/apply 的区别方式在于参数传递方式的不同;

fn.call(obj, arg1, arg2, ...), 传参数列表,以逗号隔开;

fn.call(obj, [arg1, arg2, ...]), 传参数数组;

bind 返回的是一个待执行函数,是函数柯里化的应用,而 call/apply 则是立即执行函数

思路初探
Function.prototype.myCall = function(context) {
    // 原型中 this 指向的是实例对象,所以这里指向 [Function: bar]
    console.log(this);  // [Function: bar]
    // 在传入的上下文对象中,创建一个属性,值指向方法 bar
    context.fn = this;  // foo.fn = [Function: bar]
    // 调用这个方法,此时调用者是 foo,this 指向 foo
    context.fn();
    // 执行后删除它,仅使用一次,避免该属性被其它地方使用(遍历)
    delete context.fn;
};

let foo = {
    value: 2
};

function bar() {
    console.log(this.value);
}
// bar 函数的声明等同于:var bar = new Function("console.log(this.value)");

bar.call(foo);   // 2;
call 的源码实现

初步思路有个大概,剩下的就是完善代码。

// ES6 版本
Function.prototype.myCall = function(context, ...params) {
  // ES6 函数 Rest 参数,使其可指定一个对象,接收函数的剩余参数,合成数组
  if (typeof context === "object") {
    context = context || window;
  } else {
    context = Object.create(null);
  }

  // 用 Symbol 来作属性 key 值,保持唯一性,避免冲突
  let fn = Symbol();
  context[fn] = this;
  // 将参数数组展开,作为多个参数传入
  const result = context[fn](...params);
  // 删除避免永久存在
  delete(context[fn]);
  // 函数可以有返回值
  return result;            
}

// 测试
var mine = {
    name: "以乐之名"
}

var person = {
  name: "无名氏",
  sayHi: function(msg) {
    console.log("我的名字:" + this.name + ",", msg);
  }
}

person.sayHi.myCall(mine, "很高兴认识你!");  
// 我的名字:以乐之名,很高兴认识你!

知识点补充:

ES6 新的原始数据类型 Symbol,表示独一无二的值;

Object.create(null) 创建一个空对象

// 创建一个空对象的方式

// eg.A 
let emptyObj = {};

// eg.B
let emptyObj = new Object();

// eg.C
let emptyObj = Object.create(null);

使用 Object.create(null) 创建的空对象,不会受到原型链的干扰。原型链终端指向 null,不会有构造函数,也不会有 toStringhasOwnPropertyvalueOf 等属性,这些属性来自 Object.prototype。有原型链基础的伙伴们,应该都知道,所有普通对象的原型链都会指向 Object.prototype

所以 Object.create(null) 创建的空对象比其它两种方式,更干净,不会有 Object 原型链上的属性。

ES5 版本:

自行处理参数;

自实现 Symobo

// ES5 版本

// 模拟Symbol
function getSymbol(obj) {
  var uniqAttr = "00" + Math.random();
  if (obj.hasOwnProperty(uniqAttr)) {
    // 如果已存在,则递归自调用函数
    arguments.callee(obj);
  } else {
    return uniqAttr;
  }
}

Function.prototype.myCall = function() {
  var args = arguments;
  if (!args.length) return;

  var context = [].shift.apply(args);
  context = context || window;

  var fn = getSymbol(context);
  context[fn] = this;

  // 无其它参数传入
  if (!arguments.length) {
    return context[fn];
  }

  var param = args[i];
  // 类型判断,不然 eval 运行会出错
  var paramType = typeof param;
  switch(paramType) {
    case "string":
      param = """ + param + """
    break;
    case "object":
      param = JSON.stringify(param);
    break;
  } 

  fnStr += i == args.length - 1 ? param : param + ",";

  // 借助 eval 执行
  var result = eval(fnStr);
  delete context[fn];
  return result;
}

// 测试
var mine = {
    name: "以乐之名"
}

var person = {
  name: "无名氏",
  sayHi: function(msg) {
    console.log("我的名字:" + this.name + ",", msg);
  }
}

person.sayHi.myCall(mine, "很高兴认识你!");  
// 我的名字:以乐之名,很高兴认识!
apply 的源码实现

call 的源码实现,那么 apply 就简单,两者只是传递参数方式不同而已。

Function.prototype.myApply = function(context, params) {
    // apply 与 call 的区别,第二个参数是数组,且不会有第三个参数
    if (typeof context === "object") {
        context = context || window;
    } else {
        context = Object.create(null);
    }

    let fn = Symbol();
    context[fn] = this;
    const result context[fn](...params);
    delete context[fn];
    return result;
}
bind 的源码实现

bindcall/apply 的区别就是返回的是一个待执行的函数,而不是函数的执行结果;

bind 返回的函数作为构造函数与 new 一起使用,绑定的 this 需要被忽略;

调用绑定函数时作为this参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。 —— MDN
Function.prototype.bind = function(context, ...initArgs) {
    // bind 调用的方法一定要是一个函数
    if (typeof this !== "function") {
      throw new TypeError("not a function");
    }
    let self = this;  
    let F = function() {};
    F.prototype = this.prototype;
    let bound = function(...finnalyArgs) {
      // 将前后参数合并传入
      return self.call(this instanceof F ? this : context || this, ...initArgs, ...finnalyArgs);
    }
    bound.prototype = new F();
    return bound;
}

不少伙伴还会遇到这样的追问,不使用 call/apply,如何实现 bind

骚年先别慌,不用 call/apply,不就是相当于把 call/apply 换成对应的自我实现方法,算是偷懒取个巧吧。

本篇 call/apply/bind 源码实现,算是对之前文章系列知识点的一次加深巩固。

“心中有码,前路莫慌。”

参考文档:

MDN - Function.prototype.bind()

不用call和apply方法模拟实现ES5的bind方法

更多前端基石搭建,尽在 Github,期待 Star!
https://github.com/ZengLingYong/blog

作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。

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

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

相关文章

  • JavaScript的作用域、闭包、(apply, call, bind

    摘要:闭包闭包的概念与词法域关系紧密。闭包甚至在函数已经返回后也可以获取其外部函数的变量。一种常见的闭包导致的由立即调用函数表达式解决的例子事实上结果的所有都是,而不是按顺序得出的,。 介绍 JavaScript 有一个特征————作用域。理解作用域scope可以使你的代码脱颖而出,减少错误,帮助你用它构造强大的设计模式。 什么是作用域 作用域就是在代码执行期间变量,函数和对象能被获取到的特...

    tyheist 评论0 收藏0
  • JS中的callapplybind方法详解

    摘要:不能应用下的等方法。首先我们可以通过给目标函数指定作用域来简单实现方法保存,即调用方法的目标函数考虑到函数柯里化的情况,我们可以构建一个更加健壮的这次的方法可以绑定对象,也支持在绑定的时候传参。原因是,在中,多次是无效的。 bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。 apply、call 在 javascript 中,call 和 apply 都是...

    zombieda 评论0 收藏0
  • JS基础篇--callapplybind方法详解

    摘要:首先我们可以通过给目标函数指定作用域来简单实现方法保存,即调用方法的目标函数考虑到函数柯里化的情况,我们可以构建一个更加健壮的这次的方法可以绑定对象,也支持在绑定的时候传参。原因是,在中,多次是无效的。而则会立即执行函数。 bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。 apply、call 在 javascript 中,call 和 apply 都是...

    lastSeries 评论0 收藏0
  • call,apply and bind in JavaScript

    摘要:文章尽量使用大量实例进行讲解,它们的使用场景。在严格模式下,函数被调用后,里面的默认是后面通过调用函数上的和方法,该变指向,函数里面的指向。利用,可以传入外层的上下文。同样适用的还有,里面的对象,它也是一种类数组对象。 call,apply and bind in JavaScript 在ECMAScript中,每个函数都包含两个继承而来的方法:apply() 和 call(),这两个...

    JohnLui 评论0 收藏0
  • 前端基础:call,apply,bind的的理解

    摘要:和区别其实他们的作用是一样的,只是传递的参数不一样而已。接受个参数,第一个参数指定了函数体内对象的指向,第二个参数为数组或者一个类数组。看个栗子一个有意思的事在中,多次是无效的。而则会立即执行函数。 背景 前两天在做小程序的需求的时候用到bind的时候才想起自己对这三的东西的了解比较浅薄,这个时候用的时候就有点怕。时候还是要好好学习下,理解下怎么玩。 正文 先说call 和 apply...

    netmou 评论0 收藏0

发表评论

0条评论

denson

|高级讲师

TA的文章

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