JavaScript手写系列之bind

// 示例 1:
const goods = {
    name: 'orange',
    getName: function() {
        return this.name
    }
}
const unBindGetName = goods.getName;
console.log(unBindGetName());  // this 指向的是 window,所有打印结果是 ''
const bindGetName = goods.getName.bind(goods);
console.log('bindGetName :>> ', bindGetName);   //  getName: function() { return this.name }
console.log('bindGetName :>> ', bindGetName()); // bindGetName :>>  orange


// 示例 2:
function Point(x, y) {
    this.x = x
    this.y = y
}
Point.prototype.getPos = function() {
    return `${this.x}, ${this.y}`
}
const p = new Point(1, 2)
console.log('p.getPos :>> ', p.getPos()) // p.getPos :>>  1, 2
const obj = { x: 7, y: 8 }
const BindPoint = Point.bind(obj, 3, 4)
const bindP = new BindPoint(5, 6)
console.log('BindPoint :>> ', BindPoint); // ƒ Point(x, y) { this.x = x this.y = y }
console.log('Point :>> ', Point); // ƒ Point(x, y) { this.x = x this.y = y }
console.log('bindP.getPos :>> ', bindP.getPos()) // bindP.getPos :>>  3, 4
console.log('bindP instanceof Point :>> ', bindP instanceof Point);  // bindP instanceof Point :>>  true
console.log('bindP instanceof BindPoint :>> ', bindP instanceof BindPoint); // bindP instanceof BindPoint :>>  true
console.log('BindPoint.prototype :>> ', BindPoint.prototype); // BindPoint.prototype :>>  undefined
console.log('Point.prototype :>> ', Point.prototype); // constructor: function Point(x, y) {}


/**
 * bind 源码分析
 * bind 方法创建一个新的函数
 * 在 bind() 函数被调用时
 * 新函数的 this 会被指定为 bind() 的第一个参数 
 * 其余参数将作为新函数的参数
 */
// 第一种实现方式
;(function(window) {
    if(!Function.prototype._bind) {
        Function.prototype._bind = function() {
            let fn = this,  // 谁调用 _bind 谁就是 this,也就是原始函数
                slice = Array.prototype.slice,
                context = arguments[0], // 取到 bind 函数的第一个入参,也就是要绑定的上下文环境
                args = slice.call(arguments, 1) // 截取参数,供函数调用时使用

            if(typeof fn !== 'function') {  // 如果不是函数,则提示
                throw new TypeError('必须是函数才能绑定')
            }
            return function() {
                console.log('this :>> ', this); // window
                const bindArgs = slice.call(arguments)  // bind 后的函数,传入的入参
                const allArgs = args.concat(bindArgs)   // 拼接 bind 的入参和 bind 之后的函数的入参
                return fn.apply(context, allArgs)   // 执行函数,使用 apply 改变函数的执行者
            }
        }
    }
})(window)

// 测试
const testObj = {
    language: 'JavaScript',
    getLang: function() {
        return this.language
    }
}
const unBindGetLang = testObj.getLang
const bindGetLang = unBindGetLang._bind(testObj)
console.log('bindGetLang() :>> ', bindGetLang()) // bindGetLang() :>>  JavaScript


// 第二种实现方式
;(function(window) {
    if(!Function.prototype.myBind) {
        Function.prototype.myBind = function() {
            let fn = this,                              // 谁执行 bind,this 就指向谁,this此时指向的原函数
                slice = Array.prototype.slice,          // 缓存数组的 slice 方法
                context = arguments[0],                 // 获取 bind() 函数的第一个参数,也就是要绑定的上下文环境
                args = slice.call(arguments, 1),        // 获取 bind() 函数的入参
                fNOP = function() {},                   // 定义一个空函数
                fnBind = function() {
                    const bindArgs = slice.call(arguments)  // 获取入参,转为数组
                    const allArgs = args.concat(bindArgs)   // 将 bind() 函数的入参与返回函数的入参拼接起来

                    console.log(this);  // fnBind {}   this 指向 fnBind 构造函数的实例  或者指向 Window
                    /**
                     * 此处需要判断是不是 new 操作符构造出来的实例
                     * 1.如果是 new 操作符执行的,则 this 指向的是 new 出来的实例,即 fnBind {} 
                     * 2.否则,this 指向 window 对象
                    */

                    // 第一种判断方法, __proto__ 在IE下存在兼容性问题,不推荐使用
                    const isUseNew1 = this.__proto__ === fnBind.prototype;
                    console.log('isUseNew1 :>> ', isUseNew1);     
                    
                    // 第二种判断方法
                    const isUseNew2 = this instanceof fNOP
                    console.log('isUseNew2 :>> ', isUseNew2);

                    // 第三种判断方法
                    const isUseNew3 = fNOP.prototype.isPrototypeOf(this)
                    console.log('isUseNew3 :>> ', isUseNew3);
                    
                    const fnContext = isUseNew3 ? this : context
                    return fn.apply(fnContext, allArgs)
                }

            if(typeof fn !== 'function') {
                throw new TypeError('bind 绑定的必须是函数')
            }

            // 如果原函数有 prototype 属性,就将原函数的 prototype 赋值给 fNOP的原型
            // fNOP 和 原函数原型就指向同一块内存空间
            if(fn.prototype) {
                fNOP.prototype = fn.prototype
            }

            // fnBind 的原型指向 fNOP的实例,通过原型链实现了一个继承
            fnBind.prototype = new fNOP()
            // fnBind.prototype = Object.create(fNOP.prototype) // 包装对象
            return fnBind
        }
    }
})(window)
// test 
function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.getInfo = function() {
    return `${this.name}, ${this.age}`
}
const personObj = { name: 'Jack', age: 18 }
const BindPerson = Person.myBind(personObj, 'Alen', 20)  // bindPerson.getInfo :>>  Alen, 20
const bindPerson = new BindPerson()
console.log('bindPerson.getInfo :>> ', bindPerson.getInfo()); 
console.log('bindPerson :>> ', bindPerson); // bindPerson :>>  fnBind {name: "Alen", age: 20}

// test    
function getX() {
    return this.x
}
const obj2 = {
    x: 2
}
const bindGetX = getX.myBind(obj2)
console.log('bindGetX() :>> ', bindGetX())  // bindGetX() :>>  2

参考文献:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf
https://zhuanlan.zhihu.com/p/83778815
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

 

posted @ 2021-04-08 22:26  清水渡白吟堤你如风  阅读(136)  评论(0编辑  收藏  举报