Js apply/call/bind的区别及自我实现

更新日期: 2019-07-11 阅读: 2.4k 标签: 区别

call/apply/bind 日常编码中被开发者用来实现 “对象冒充”,也即 “显示绑定 this“。面试题:“call/apply/bind源码实现”,事实上是对 JavaScript 基础知识的一个综合考核。

相关知识点:

  1. 作用域;
  2. this 指向;
  3. 函数柯里化;
  4. 原型与原型链;


call/apply/bind 的区别

  1. 三者都可用于显示绑定 this;
  2. call/apply 的区别方式在于参数传递方式的不同;

    • fn.call(obj, arg1, arg2, ...), 传参数列表,以逗号隔开;
    • fn.call(obj, [arg1, arg2, ...]), 传参数数组;
  3. 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, '很高兴认识你!');  
// 我的名字:以乐之名,很高兴认识你!

知识点补充:

  1. ES6 新的原始数据类型 Symbol,表示独一无二的值;
  2. Object.create(null) 创建一个空对象
// 创建一个空对象的方式

// eg.A 
let emptyObj = {};

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

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

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

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

ES5 版本:

  1. 自行处理参数;
  2. 自实现 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 的源码实现

  1. bind 与 call/apply 的区别就是返回的是一个待执行的函数,而不是函数的执行结果;
  2. 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 源码实现,算是对之前文章系列知识点的一次加深巩固。

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

来自:https://segmentfault.com/a/1190000020033761


本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/4631

相关推荐

AR / MR / VR / XR有什么区别?

VR能让人完全沉浸在虚拟环境中;AR能创建一个叠加在虚拟内容的世界,但不能与真实环境交互; MR则是虚拟与现实的混合体,它能创造出可以与真实环境交互的虚拟物体。最后,XR则是包括三种“现实”(AR,VR,MR)的术语。

Js插件、 组件、类库、框架的区别

框架和类库等概念的出现都是源于人们对复用的渴望。“不要重复发明轮子”,成了软件界的一句经典名言。从最初的单个函数源代码的复用,到面向对象中类的复用(通常以类库的形式体现)

理解screenX,clientX,pageX,offsetX,pageXoffset的区别

event.screenX、event.screenY鼠标相对于用户显示器屏幕左上角的X,Y坐标。标准事件和IE事件都定义了这2个属性,event.clientX、event.clientY鼠标相对于浏览器可视区域的X,Y坐标

初中高级工程师的区别?

我们知道程序员分 初级,中级, 高级, 这个概念, 并不是以工作年限作为分类依据的。 也就是说, 级别和你工作多少年没有必然的联系。一个初级工程师可能工作很多年依然是初级工程师, 也有的工程师, 工作短短两三年, 就跻身高级工程师的行列。

互联网、局域网、万维网三者区别?

互联网是指多台设备(如计算机、手机等)通过特定通信协议(TCP/IP、IPX/SPX、NetBIOS、NetBEUI、Apple Talk)组成的网络。一般可分为以下三种:局域网LAN(Local Area Network):一般不大于10公里,而且通常只使用一种传输介质

js中.和[]的区别

在js中,对象属于是键值对的集合,在上面这个代码中,name就属于是key,而‘残梦‘就是value。总之一句话: []更强大,.就是用起来更加习惯一些,一开始用[]的时候总是会当成数组,需要注意一下

CSS中zoom和scale的差异

zoom和scale这两个东西都是用于对元素的缩放,但两者除了兼容性之外还有一些不同的地方。zoom缩放会将元素保持在左上角,而scale默认是中间位置,可以通过transform-origin来设置。

*.min.js跟*.js的区别

js是JavaScript 源码文件, .min.js是压缩版的js文件。减小体积 .min.js文件经过压缩,相对编译前的js文件体积较小,传输效率快。防止窥视和窃取源代码

package.json中^和~的区别

webpack 项目的package.json 文件列出了项目所依赖的插件和库,同时也给出了对应的版本说明,但是在版本说明前面还有个符号:‘^‘(插入符号)和‘~‘(波浪符号),总结了下他们之间的区别:

PTN与SDH的区别?

SDH是基于TDM技术,主要用于传输语音,此外采用GFP封装来传输IP包,物理介质为光纤。PTN是采用DWDM技术,主要用于传输IP包、以太网帧,此外采用MPLS-TP技术来实现PWE3伪线

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!