call-apply-bind-new


call-apply-bind-new

call/apply/bind

三个函数均可以改变函数的this指向

call(context,arg1,arg2…) ,改变函数的this指向 并立即执行

apply(context,[arg1,arg2…]) ,改变函数的this指向 并立即执行,与call接收的参数不同

bind(context,arg1,arg2…) ,改变函数的this指向

call(context,arg1,arg2…)

ES6

Function.prototype.call2 = function (context = window,...args) { // 不能写箭头函数,不然this出问题
    context.fn = this; // 可以防止变量覆盖 const key = Symbol();context[key] = this
    let result = context.fn(...args)
    delete context.fn
    return result;
}

ES3

Function.prototype.call2 = function (context) { // 不能写箭头函数,不然this出问题
    context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

存在fn属性被覆盖的问题,Object.hasOwnProperty检查一下有没有成员,有得话就先保存下来,弄完后再赋值回去

apply(context,[arg1,arg2…])

ES6

Function.prototype.apply2 = function (context = window, arr = []) { // 多了一个默认空数组
    context.fn = this;
    let result = context.fn(...arr)
    delete context.fn
    return result;
}

ES3

Function.prototype.apply2 = function (context, arr) {
    var context = Object(context) || window;
    context.fn = this;

    var args = [];  // arguments => arr,1 => 0
    for (var i = 0, len = arr ? arr.length : 0; i < len; i++) {
        args.push('arr[' + i + ']');
    }
    var result = eval('context.fn(' + args + ')')

    delete context.fn
    return result;
}

bind(context,arg1,arg2…)

源码(不考虑new的情况)

ES6

Function.prototype.bind2 = function (context) {
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function');
    }

    const args = [...arguments].slice(1)
    const self = this;
    return function fn() { // 用闭包保留了 原函数self
        return self.apply(context, args.concat(...arguments) );
    };
}

ES3

Function.prototype.bind2 = function bind(context){
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function');
    }
    var self = this;
    var args = [].slice.call(arguments, 1);
    return function fn(){
        var boundArgs = [].slice.call(arguments);
        return self.apply(context, args.concat(boundArgs));
    };
}

bind的三条特性

第三条尤为麻烦

1.函数.bind(context,arg1,arg2…) 返回 一个 this绑定context 新函数

2.可以在bind时 传入 函数的 部分参数

3.可以 new新函数 创建对象,此时 构造函数是 旧函数, 绑定的新context失效,但bind时 传入的参数有效

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

源码(简单实现带new效果)

ES6

Function.prototype.bind2 = function (context) {
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function');
    }

    const args = [...arguments].slice(1)
    const self = this;
    return function fn() { // 用闭包保留了 原函数self
        return self.apply( this instanceof fn ? this : context, args.concat(...arguments) );
    };
}
}

注意 if (this instanceof fn) {}

new 函数 的时候,会将当前 函数 的this绑定 由其自己为构造函数,创建的新对象,再执行

换句话说,当 this 指向的是 该函数的实例 时,代表正在进行new操作。

new 新函数 时,需要使用 原函数 绑定 new 的时候创建的 新对象 执行原函数,也就是apply(this)

正常调用 新函数时,需要 apply(context) 绑定 context进行调用。

源码(完全实现)

过于复杂,先贴着,待续…

// 第三版 实现new调用
Function.prototype.bind2 = function bind(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + ' must be a function');
    }
    // 存储调用bind的函数本身
    var self = this;
    // 去除thisArg的其他参数 转成数组
    var args = [].slice.call(arguments, 1);
    var bound = function(){
        // bind返回的函数 的参数转成数组
        var boundArgs = [].slice.call(arguments);
        var finalArgs = args.concat(boundArgs);
        // new 调用时,其实this instanceof bound判断也不是很准确。es6 new.target就是解决这一问题的。
        if(this instanceof bound){
            // 这里是实现上文描述的 new 的第 1, 2, 4 步
            // 1.创建一个全新的对象
            // 2.并且执行[[Prototype]]链接
            // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
            // self可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作。
            if(self.prototype){
                // ES5 提供的方案 Object.create()
                // bound.prototype = Object.create(self.prototype);
                // 但 既然是模拟ES5的bind,那浏览器也基本没有实现Object.create()
                // 所以采用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
                function Empty(){}
                Empty.prototype = self.prototype;
                bound.prototype = new Empty();
            }
            // 这里是实现上文描述的 new 的第 3 步
            // 3.生成的新对象会绑定到函数调用的`this`。
            var result = self.apply(this, finalArgs);
            // 这里是实现上文描述的 new 的第 5 步
            // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),
            // 那么`new`表达式中的函数调用会自动返回这个新的对象。
            var isObject = typeof result === 'object' && result !== null;
            var isFunction = typeof result === 'function';
            if(isObject || isFunction){
                return result;
            }
            return this;
        }
        else{
            // apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果
            return self.apply(thisArg, finalArgs);
        }
    };
    return bound;
}

JavaScript深入之bind的模拟实现

若川 面试官问:能否模拟实现JS的bind方法

手写new

ES5

function myNew(constructor, ...args) {
    if (typeof constructor !== 'function') {
        throw new Error('constructor must be a function!');
    }

    const obj = Object.create(constructor.prototype); // 以构造函数的prototype为原型创建一个新对象
    const res = constructor.apply(obj, args); // 以新对象为this调用构造函数,取得返回值

    const isObject = typeof res === 'object' && res !== null;
    const isFunction = typeof res === 'function';

    return isObject || isFunction ? res : obj;
}
  1. 创建一个 以该 构造函数.prototype 为原型 的 新对象

  2. 以新对象为this,调用 该构造函数 ,内部可通过this对 新对象 添加赋值属性

  3. 如果 构造函数 有返回值且是 对象或函数(也是对象),则前面白干,以返回值为最终结果

前两条 保证了 new 新对象,

既可以访问 构造函数.prototype(原型对象)中的属性,

又可以访问 构造函数调用时通过 this给 新对象 赋值的属性。

proto ,绝大部分浏览器都支持这个非标准的方法访问原型,
然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,
与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,
可以理解成返回了 Object.getPrototypeOf(obj)。

冴羽大佬的评论区是必看的
JavaScript深入之new的模拟实现

面试官问:能否模拟实现JS的new操作符


文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
 上一篇
手写Promise 手写Promise
一篇简单的Promise实现,离A+规范还远,但是对于理解Promise原理,绰绰有余了....从实现 同步Promise,到处理异步,再到处理链式调用,层层递进,非常友好。
2022-06-23
下一篇 
JS执行过程 JS执行过程
JS执行以代码段为单位,分为 三个阶段:语法分析 /创建阶段/ 执行阶段。作用域链是JS引擎查找变量的规则,原型链是JS引擎查找对象属性的规则
2022-06-14
  目录