likes
comments
collection
share

手写call,apply,bind,new

作者站长头像
站长
· 阅读数 21

手写call,apply,bind,new

引言

  • 时隔一年,再次手写这些枯燥而又无聊的api。让我有了新领悟。为什么要将这些api放在一起呢? 因为他们都和this有着密不可分的关系。众所周知,this实际上是在函数被调用时发生的绑定,他的指向完全取决于函数在哪里被调用(被调用的时执行上下文)。关于this的详细介绍,请看这篇文章
  • 那这就出现一个问题,如果不想遵循this的绑定规则,要怎么做呢? 可以利用call,apply,bind。来强行绑定this
  • this也和new 有很大的关联。但是我想从构造函数和工厂函数做对比,来引出new中的this

call的特点

  • 可以改变我们当前函数this指向

  • 还会让当前函数执行

  • 接受的是一个参数列表

手写call

Function.prototype._call = function (ctx, ...args) {
    let fn = Symbol();
    let context = ctx ? Object(ctx) : window;
    context.fn = this;
    let res = args.length ? context.fn(...args) : context.fn(); // 判断是否传参
    delete context.fn;
    return res;
  };

  let obj2 = {
    a: 2,
  };
  let obj1 = {
    a: 1,
    getName: function (b, c) {
      console.log(this.a);
      console.log(b);
      console.log(c);
      return this.a + b + c;
    },
  };
  console.log(obj1.getName._call(obj2, 3, 4)); // 9

思路讲解

  • 根据call的特点,就是改变this指向,并且让当前函数执行。
  • 根据传入的上下文(如果传入的是基本数据类型,并且是真值,就用对象包装一下,否则,传入的就是默认执行window),让其拥有属性,让这个属性去执行。 ctx ? Object(ctx) : window
  • 改变this指向: 将当前的this赋值给传入的上下文 context.fn = this;
  • 让当前函数执行(需要判断是否传参) args.length ? context.fn(...args) : context.fn()
  • 将当前函数执行的结果返回 return res
  • 删除我们构造的假执行的函数: delete context.fn

apply的特点

  • 可以改变我们当前函数this指向
  • 还会让当前函数执行
  • 接受的是一个数组(或一个类数组对象

那手写apply,只要判断传入参数,是否是数组即可。其余跟call实现一样

手写apply

  Function.prototype._apply = function (ctx, args = []) {
    if (!Array.isArray(args)) {
      throw new Error('apply need Array');
    }  
    let fn = Symbol();
    let context = ctx ? Object(ctx) : window;
    context.fn = this;
    let res = args.length ? context.fn(...args) : context.fn();
    delete context.fn;
    return res;
  };

  let obj2 = {
    a: 2,
  };
  let obj1 = {
    a: 1,
    getName: function (b, c) {
      console.log(this.a);
      console.log(b);
      console.log(c);
      return this.a + b + c;
    },
  };
  console.log(obj1.getName._apply(obj2, [3, 4])); // 9

思路讲解

  • 和call基本一直,就是要注意apply接受的是一个数组(或一个类数组对象
  • 防止无参数,给一个默认参数类型是数组,args = []
  • 如果是非数组,则抛出错误 throw new Error

bind的特点

  • bind方法可以绑定this指向

  • bind方法返回一个绑定后的函数,(高阶函数)

  • 如果绑定的函数被new了,当前函数的this,就是当前的实例

手写bind

Function.prototype._bind = function (ctx, ...bindArgs) {
    let that = this;
    return function () {
      return that.apply(ctx, bindArgs);
    };
  };

  let obj1 = {
    age: '2',
  };
  let obj2 = {
    age: '88',
    getInfo: function (name) {
      return `${name} 今年${this.age} 岁`;
    },
  };
  let p = obj2.getInfo._bind(obj1, '小明');
  console.log('p', p());

思路讲解

  • bind()  方法创建一个新的函数所以要return 一个 function,等待调用时执行.里面返回了一个函数,就是高阶函数的用法
  • 为了获取原始函数的this,在内部使用了一个变量that来保存,用到了闭包。其实还可以用箭头函数 let that = this;

关于bind的其他考量

  • 由于bind只是改变this指向,并不执行。这就给函数调用增加了一些逻辑判断。
  1. 如果调用者,又传入参数该怎么办?
  2. 如果函数用new来实例化,内部的this改怎么处理?
  3. 如果函数要在原型上追加属性,该怎么处理?
调用者传入参数处理
Function.prototype._bind = function (ctx, ...bindArgs) { // bind绑定着参数获取
    let that = this;
    return function (...args) { // 调用者传入参数获取
      return that.apply(ctx, bindArgs.concat(args));  // 只要将二者进行拼接即可
    };
  };

  let obj1 = {
    age: '2',
  };
  let obj2 = {
    age: '88',
    getInfo: function (name) {
      console.log('arg', arguments); // [Arguments] { '0': '小明', '1': '调用时传入参数' }
      console.log('name', name); // name 小明。
      return `${name} 今年${this.age} 岁`;
    },
  };
  let p = obj2.getInfo._bind(obj1, '小明'); // 小明 今年2 岁
  console.log('p', p('调用时传入参数'));

tips
  • 如果只有一个形参参数接收,但是传入了两个实参,此时会默认取第一个实参。如果想改变实参获取,可以在concat更换位置,或者在使用时,用过索引取形参
函数用new来实例化,并且在原型上进行操作
  Function.prototype._bind = function (ctx, ...bindArgs) {
    let that = this;
    function temp() {} // Object.create 原理
    function fBind(...args) {
      return that.apply(
        // 如果被new调用,this是fBind的实例
        this instanceof fBind ? this : ctx,
        args.concat(bindArgs)
      );
    }
    // 维护fBind的原型
    temp.prototype = this.prototype;
    fBind.prototype = new temp();
    return fBind;
  };

  let obj1 = {
    age: '2',
  };
  let obj2 = {
    age: '88',
    getInfo: function (name) {
      console.log('arg', arguments);
      console.log('name', name);
      return `${name} 今年${this.age} 岁`;
    },
  };
  let p = obj2.getInfo._bind(obj1, '小明');
  console.log('p', p); // [Function: fBind]
  let pp = new p();
  console.log('pp', pp); //  getInfo {}

关于new

说起new,就不得不说说构造函数,详细链接如下

平常封装的函数,基本都可以说是工厂函数,比如我们对接口的封装

const res = await Net.upload('/Upload/upload', previewData);

我们并不会去关心Net.upload是怎么实现的,只需要传入相应的参数('/Upload/upload', previewData) ,就可以做数据请求。这就是工厂函数。

再比如,我们要得到一个对象

  function getObj(a, b) {
    let obj = {};
    obj.a = a;
    obj.b = b;
    return obj;
  }
  const obj = getObj('aa', 'bb');
  console.log(obj); // { a: 'aa', b: 'bb' }

我们并不需要关心内部怎么实现,只需要传入响应的参数即可。但是这也会有一个问题,就是每次都要声明一个对象,对象赋值,并且,返回这个对象,比较繁琐。new替我们做了这些事!!! 使用new 来做函数的构造调用

function getObj(a, b) {
    this.a = a;
    this.b = b;
  }
  const obj = new getObj('aa', 'bb');
  console.log(obj);

new的特点

  1. 类比工厂函数,可以看出,在构造函数内部,其实每次也要新创建一个对象

  2. 并且会默认把当前的this,指向新创建对象的this

  3. 原型链会做连接

  4. 如果返回值是隐式返回,那么就返回新创建的对象,否则返回显示返回的对象

手写new

function _new() {
 let Constructor = [].shift.call(arguments);
 if (typeof Constructor !== 'function') {
   throw new Error('The first argument of new must be a function');
 }
 let obj = {}; // 创建/ 构造一个对象
 Object.setPrototypeOf(obj, Constructor.prototype);
 let res = Constructor.apply(obj, arguments);
 return res instanceof Object ? res : obj;
}
function Test(name, age) {
 this.name = name;
 this.age = age;
}
Test.prototype.sayName = function () {
 console.log(this.name);
};

const t = _new(Test, 'wd', 7);
console.log(t); // Test { name: 'wd', age: 7 }

思路讲解

  • 传入的必须是个函数,才可以做函数的构造调用
  • [].shift.call(arguments) 获取第一个参数,第一个参数就是传入的函数。此时arguments的参数就是剩余的所有参数
  • Object.setPrototypeOf 创建的对象和传入的函数,做一个关联
  • Constructor.apply(obj, arguments) 改变this指向,并且apply会调用传入的函数,此时默认传入的函数是没有返回值的

手写call,apply,bind,new

如果传入的函数,有返回值,

手写call,apply,bind,new

如果是返回对象,那么就要做检测。如果调用的函数有返回值,并且是对象,就要返回其所返回的对象。 return res instanceof Object ? res : obj;

否则就返回 创建的新对象 obj

return res instanceof Object ? res : obj;

结束

转载自:https://juejin.cn/post/7134679154926043172
评论
请登录