zoukankan      html  css  js  c++  java
  • JavaScript内置一些方法的实现原理--new关键字,call/apply/bind方法--实现

    先学习下new操作符吧

    new关键字调用函数的心路历程:

    1.创建一个新对象

    2.将函数的作用域赋给新对象(this就指向这个对象)

    3.执行函数中的代码

    4.返回这个对象

    根据这个的思路,来实现一个简单的new操作吧,代码演示:

     1 function myNew(Func, ...args) {
     2   if (typeof Func !== 'function') throw new Error(`${Func} is not a constructor`);
     3   const obj = Object.create(Func.prototype);
     4   const res = Func.apply(obj, args);
     5   if (res instanceof Object) return res;
     6   return obj;
     7 }
     8 function Person(name, age) {
     9   this.name = name;
    10   this.age = age;
    11 }
    12 const p1 = new Person('xm', 20);
    13 const p2 = myNew(Person, 'xm', 20);

    首先,先判断传进来的第一个参数是不是函数,不是函数抛出错误。

    接着以传进来的函数的原型对象为原型创建一个新对象。

    这步相当于obj.__proto__ = Func.prototype或者Object.setPrototypeOf(obj, Func.prototype)。

    (Object.create()是推荐用法,ie11以下不支持上面那些东西、const,ie11不支持...)

    调用函数,通过apply方法把函数的this绑定到obj。如果函数有返回值,且为对象,则返回该对象。

    否则返回obj。

    稍微兼容一点的写法(到ie9):

     1 function myNew(Func) {
     2   if (typeof Func !== 'function') throw new Error( Func + 'is not a constructor');
     3   var obj = Object.create(Func.prototype);
     4   var args = Array.prototype.slice.call(arguments, 1);
     5   var res = Func.apply(obj, args);
     6   if (res instanceof Object) return res;
     7   return obj;
     8 }
     9 function Person(name, age) {
    10   this.name = name;
    11   this.age = age;
    12 }
    13 var p1 = new Person('xm', 20);
    14 var p2 = myNew(Person, 'xm', 20);

    验证:

    call方法

    call方法最常见的用法就是改变函数的this指向。

    该方法的第一个参数为函数的this指向。后面的参数为函数的参数,按顺序传入。

    根据这个思路,来实现这样一个功能简单的call方法吧。代码演示:

     1 Function.prototype.myCall = function(context, ...args) {
     2   let self = this;
     3   context = context || window;
     4   Object.defineProperty(context, 'myFn', {
     5     configurable: true,
     6     get() {
     7       return self;
     8     }
     9   });
    10   const res = context.myFn(...args);
    11   delete context.myFn;
    12   return res;
    13 };

    以上代码,在Function的原型对象上添加一个myCall方法,就可以实现fn.myCall()如此模样的操作了。

    接着定义一个变量保存方法函数内部this,this指向调用该方法的函数。

    然后做一个简单的短路操作,如果传进来的第一个参数是undefined、null,则指向window。

    当然,如果瞎传,那就只能下一行执行时报错了。

    接着在context上添加一个属性,并把该属性设置成可以删除的,设置get方法。

    访问该属性时触发get方法,返回的是调用myCall方法的函数。接下来调用函数,返回值保存到res。

    最后删除context.myFn属性,返回res。

    验证:

    稍微兼容一些的写法:

     1 Function.prototype.myCall = Function.prototype.call || function(context) {
     2   var self = this, args = [], res, i, len;
     3   context = context || window;
     4   Object.defineProperty(context, 'myFn', {
     5     configurable: true,
     6     get() {
     7       return self;
     8     }
     9   });
    10   for (i = 1, len = arguments.length; i < len; i++) {
    11     args.push('arguments[' + i + ']');
    12   }
    13   res = eval('context.myFn(' + args + ')');
    14   delete context.myFn;
    15   return res;
    16 };

    当然,测试的时候先把前面的短路操作去掉。。

    apply方法

    apply方法与call方法很相似,只在传参上有点区别。apply第二个参数是数组或类数组对象。

     接下来就来实现一个功能相对单一的apply方法吧。代码演示:

     1 Function.prototype.myApply = function(context, arr) {
     2   let self = this;
     3   context = context || window;
     4   Object.defineProperty(context, 'myFn', {
     5     configurable: true,
     6     get() {
     7       return self;
     8     }
     9   });
    10   const res = context.myFn(...arr);
    11   delete context.myFn;
    12   return res;
    13 };

    几乎和call方法的实现一模一样。。

    稍微兼容一些的写法:

     1 Function.prototype.myApply = Function.prototype.apply || function(context) {
     2   var self = this, args = [], _args = [], res, i, len;
     3   context = context || window;
     4   Object.defineProperty(context, 'myFn', {
     5     configurable: true,
     6     get() {
     7       return self;
     8     }
     9   });
    10   for (i = 0, len = arguments[1].length; i < len; i++) {
    11     args.push('_args[' + i + ']');
    12     _args.push(arguments[1][i]);
    13   }
    14   res = eval('context.myFn(' + args + ')');
    15   delete context.myFn;
    16   return res;
    17 };

    需要注意的是,传进的参数只有两个,第二个是数组或类数组,像myCall方法中传入eval函数的字符串直接映射arguments对象是行不通的。

    可以再创建一个数组,把传进来的第二个参数全部push进去,eval函数的字符串直接映射该数组。

    当然,测试的时候先把前面的短路操作去掉。。

    bind方法

    bind方法略有不同,函数调用bind方法,返回的是一个函数。当函数只是普通调用时,this指向bind方法的第一个参数。

    如果返回的函数被当做构造函数调用时,前面绑定的this又无效了,此时指向new操作符创建的对象。

    如:

    1 function person() {
    2   console.log(this); 
    3 }
    4 const P1 = person.bind({name: 'xm'});
    5 P1();// {name: 'xm'}
    6 new P1();// person实例

    还需要注意的是,当返回的函数被当做对象的方法调用时,此时this仍然指向bind方法绑定的对象。如:

    1 const obj = {
    2   P1  
    3 };
    4 obj.P1();// {name: 'xm'}

    接下来,来实现这样的一个bind方法吧。代码演示:

     1 Function.prototype.myBind = function(context, ...args) {
     2   const self = this;
     3   context = context || window;
     4   const Bound = function() {
     5     const _args = [...args, ...arguments];
     6     let _context = context;
     7     if (this instanceof Bound) _context = this;
     8     return self.apply(_context, _args);
     9   }
    10   const _Fn = function () {};
    11   _Fn.prototype = this.prototype;
    12   Bound.prototype = new _Fn();
    13   return Bound;
    14 };

    以上代码,在Function的原型对象上添加一个myBind方法,就可以实现fn.myBind()如此模样的操作了。

    接着保存函数的this,这个this指向调用myBind()方法的函数。

    然后简单处理下传进来的第一个参数,为null、undefined时指向window。

    接下来就是创建一个Bound函数,这个Bound函数是一个闭包,它可以访问外层函数的变量。

    最后它是要作为myBind()方法的返回值,返回出去的。

    在Bound函数里,第五行代码先处理了下参数。除第一个context参数,其他参数有时候会在myBind方法里传,有时候会在返回的函数里传。

    这里不管三七二十一,都转成数组,然后拼接成一个数组。然后通过apply方法传参。(使用eval函数也行,就是麻烦了些)

    第七行代码判断this 和 Bound的关系,如果Bound函数被new操作符调用,则函数内部的this指向Bound的实例(即new创建的对象)。

    此时instanceof会返回真,然后这里处理下_context参数,把this赋给_context。

    最后通过apply方法调用self(即外层函数保存的this),最后把函数返回值return出去。

    测试代码:

    1 function person(name, age) {
    2   console.log(this);
    3   console.log(name, age);
    4 }
    5 const Bound = person.myBind({name: 'xm'}, 'xh' );
    6 const person1 = new Bound(20);

    以上代码,person函数先调用myBind()方法,并把返回的函数(Bound)保存到Bound。

    然后通过new调用Bound函数。结果:

    对比:

    方法没有大的问题,基本是实现了。

    然后,这三行代码的作用是处理Bound函数的原型链。这样做的一个好处是,

    Bound函数原型对象是空函数_Fn的一个实例。可以随意扩展。_Fn函数的原型对象又是person函数的原型对象。

    这就等于Bound函数的原型对象的原型指针指向了person函数的原型对象。。。代码描述就是  

    因此,Bound函数的实例不仅拥有Bound.prototype上的方法和属性,还拥有person函数原型对象的方法和属性。

    原型链,如图:

    验证:

    稍微兼容一些的写法:

     1 Function.prototype.myBind = Function.prototype.bind || function(context) {
     2   var self = this;
     3   var args = Array.prototype.slice.call(arguments, 1);
     4   context = context || window;
     5   var Bound = function() {
     6     var _args = Array.prototype.slice.call(arguments, 0);
     7     var _context = context;
     8     if (this instanceof Bound) _context = this;
     9     return self.apply(_context, args.concat(_args));
    10   }
    11   var _Fn = function () {};
    12   _Fn.prototype = this.prototype;
    13   Bound.prototype = new _Fn();
    14   return Bound;
    15 };
  • 相关阅读:
    SpringBoot 断点调试无效问题解决
    oracle数据库入门
    IPFS入门
    Spring安全参考
    什么是内存泄漏?该如何检测?又该如何解决?
    coredump
    同一个程序在一个系统中可以跑起来,在另外一个系统上跑不起来
    dpkg 强制安装deb文件
    日志文件丢失
    文件句柄资源
  • 原文地址:https://www.cnblogs.com/caimuguodexiaohongmao/p/11178326.html
Copyright © 2011-2022 走看看