bind / call / apply 用法整理 以及 自定义实现

共同点:改变 this 指向

区别:

call 和 apply 的区别:传参的方式不同

  • call(obj, param1, param2, param3......)
  • apply(obj, [param1, param2, param3......])

bind 与 call / apply 的区别:

  • bind 返回的是一个新的函数,需要再调用一下才会执行
  • bind 的传参方式同 call, 即:bind(obj, param1, param2, ...)
  • call / apply 是立即执行,bind 返回的是一个函数,便于稍后调用

需要注意的: 如果第一个参数是 null / undefined , 则在函数体内会指向 全局对象 ,在浏览器中指向 window


举个栗子:

var user = {
    name: 'zhang',
    myAge: this.age,
    myFun: function() {
        console.log(`My name is ${this.name}, ${this.age} years old.`);
    },
    myFun2(from, to) {
        console.log(`My name is ${this.name}, ${this.age} years old. I'm from ${from} , I wanna go to ${to}`);
    }
};

var db = {
    name: 'Emma',
    age: 66
}


user.myFun.call(db);
user.myFun.apply(db);
user.myFun.bind(db)();
// My name is Emma, 66 years old.

user.myFun2.call(db, 'beijing', 'qinghai');
user.myFun2.apply(db, ['beijing', 'qinghai']);
user.myFun2.bind(db, 'beijing', 'qinghai')();
// My name is Emma, 66 years old. I'm from beijing , I wanna go to qinghai

call 和 apply 实例: 

(1)追加数组:

var arr1 = [1,2,3,4];
var arr2 = ['a', 'b', 'c', 'd'];
var arr3 = [];

console.log(Array.prototype.push.call(arr3, ...arr1, ...arr2)); 
// arr3 [1,   2,   3,   4, 'a', 'b', 'c', 'd']
console.log(Array.prototype.push.apply(arr1, arr2));
// arr1 [1,   2,   3,   4, 'a', 'b', 'c', 'd']
console.log(Array.prototype.push.bind(arr3, ...arr1, ...arr2)());
// arr3 [1,   2,   3,   4, 'a', 'b', 'c', 'd']

 (2)获取数组中的最大/小值:

console.log(Math.max.call(Math, ...randomArr)); // 45
console.log(Math.min.apply(Math, randomArr)); //1

(3)判断数据类型

function getDataType(data) {
    return Object.prototype.toString.call(data);
    // 返回值:
    // "[object Object]"
    // "[object Array]"
    // "[object Function]"
    // "[object String]"
    // "[object Number]"
    // "[object Null]"
    // "[object Undefined]"
}

(4)类数组(伪数组对象结构,有length属性,但是不能使用数组的 push / pop 等方法) 转换为 数组

ps: 常见的伪数组有:arguments 对象 、 调用 document.getElementsByTagName / document.childNodes 等获取的节点数组等。

Array.prototype.slice.call(伪数组对象);
eg.
let arr = document.getElementsByTagName("*"); // Array.isArray(arr) --> false
let arr2 = Array.prototype.slice.call(arr); // Array.isArray(arr2) --> true

再举个例子:定义一个 log 方法,可以代理 console.log 方法

function log() {
    //方式一
    console.log(...arguments);  

    //方式二
    console.log.apply(console, arguments); 

    // 方式三:要求在每个log前都加上(app)标识
    let args = Array.prototype.slice.call(arguments); 
    args.unshift('(app)');
    console.log.apply(null, args);
}

bind 的使用:

(1)绑定函数(修正 this 指向):

eg1.

let myWrite = document.write;
myWrite('write sth.'); // Uncaught TypeError: Illegal invocation(不合法的调用)

//可修改为酱紫:

myWrite.bind(document)('write sth.'); //调用myWrite的时候,将this指向为document

eg2.

var myModule = {
    num: 8,
    getNum() {
        console.log('this.num is ' + this.num);
    }
}
this.num = 9;
let getNum = myModule.getNum;
getNum(); // this.num is 9
getNum.bind(myModule)(); // this.num is 8

eg3.

//单体模式,通常会用 _this / that / self 等保存 this, 以便以后可以继续引用它
//通常的写法:
var foo = {
    bar: 1,
    eventBind() {
        let that = this;
        $('.class').off().on("click", function(ev) {
            console.log(that.bar);
        });
    }
}

//用 bind 可以更加优雅的解決这个问题
var foo = {
    bar: 1,
    eventBind() {
        $('.class').off().on("click", function(ev) {
            console.log(this.bar);
        }.bind(this));
    }
}

(2)偏函数:将一个函数的某些参数先固化(也就是设置默认参数),返回一个新函数,在新函数中继续接收剩余的参数,这样调用这个新函数会更简单。

function list() {
    return Array.prototype.slice.apply(arguments);
}

let list1 = list(1,2,3,4); //[ 1, 2, 3, 4 ]
let partialList = list.bind(null, 66);
partialList(5, 6, 8); //[ 66, 5, 6, 8 ]
partialList(8, 8, 6); //[ 66, 8, 8, 6 ]

(3)和 setTimeout / setInterval 一起使用:

function Bloomer() {
    this.petalCount = Math.floor(Math.random() * 12) + 1;
}

Bloomer.prototype.declare = function() {
    console.log(`我有 ${this.petalCount} 片花瓣!`);
}

Bloomer.prototype.bloom = function() {
    setTimeout(this.declare.bind(this),1000);
}

let bloomer = new Bloomer();
bloomer.bloom();

(4)绑定函数作为构造函数

function Point(x, y) {
    this.x = x;
    this.y = y;
}


Point.prototype.toString = function() {
    console.log(`x is ${this.x}, y is ${this.y}`);
}

let emptyObj = {};
let BindPoint = Point.bind(emptyObj, 0);
let bp = new BindPoint(10);

实现一个 bind 函数

  • 什么是 函数柯里化(Function currying)?
  • 把接受多个参数的函数转化为接受一个单一参数(最初函数的第一个参数)的函数,并且返回结果为接受余下参数的新函数
  • 什么是 偏函数(Partial function)?
  • 将一个函数的一些参数固化(设置默认参数),返回一个新函数,在新函数中接受剩余参数。

第一次实现: 简单实现:

Function.prototype.myBind = function(context) {
    let self = this;
    return function() {
        return self.apply(context, arguments);
    }
}

上面的实现方法不能实现绑定时传参的功能

第二次实现: 实现自定义的 bind 函数能够传递默认参数(即实现柯里化),返回的新函数能够接收剩余参数

Function.prototype.myBind2 = function (context) {
    let self = this;
    let outerArgs = Array.prototype.slice.call(arguments, 1);
    return function () {
        let innerArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, [...outerArgs, ...innerArgs]);
    }
}

或者:

Function.prototype.myBind2 = function (context) {
    let self = this;
    let outerArgs = Array.prototype.slice.call(arguments, 1);
    return function () {
        let innerArgs = Array.prototype.slice.call(arguments);
        let finalArgs = outerArgs.concat(innerArgs);
        return self.apply(context, finalArgs);
    }
}

3. 第三次实现:

上面的实现方法虽然 能够实现原生bind的基本功能,但是不能实现下面的功能:

Foo.prototype.food = 'foo-food';
var Foo = function() {
    console.log(this.food);
}

var obj = {
    food: 'obj-food'
};
//原生的 bind 方法的执行结果如下 
var BFoo = Foo.bind(obj);
new BFoo(); // foo-food 当通过new操作符来执行 BFoo 时,this指向的是Foo
BFoo(); // obj-foo 当把BFoo当作普通函数调用时,this 指向的是给定的对象


//第二种自定义bind实现方法的执行结果如下 
var BFoo2 = Foo.myBind2(obj);
new BFoo2(); // obj-food
BFoo2(); // obj-food

为了解決上面的问题,将第二种方式改写为:

Function.prototype.myBind4 = function(context) {
    let outerArgs = Array.prototype.slice.call(arguments, 1);
    let self = this;
    if (typeof self !== 'function') {
        throw new TypeError(`${self} is not a function!`);
    }
    var F = function() {}; // 作为中间层
    let bound = function() {
        let innerArgs = Array.prototype.slice.call(arguments);
        let args = outerArgs.concat(innerArgs);
        // Q: ctx 为什么要加个判断呢?
        // A: 当 this 是 bound 的实例的时候(即 通过 new 操作符来执行bound函数的时候),
        //    给定的(绑定的)对象是 bound 
        //   (还应将 bound 的原型指向 self.prototype,
        //    这样当使用 new 操作符的时候,指向的就会是实例本身,而不是bind函数绑定的对象),
        //    当 this 不是 bound 的实例的时候(通过普通函数调用),ctx 应该指向的是给定的对象
        let ctx = this instanceof bound? this : context;
        return self.apply(ctx, args);
    }
    F.prototype = self.prototype;
    bound.prototype = new F(); // F 函数作为中间层,防止修改 bound 原型上的属性时,将 self.prototype上的属性修改
    return bound;
}

最终版(在上面的基础上再加些细节上的判断):

if (!Function.prototype.bind) {
    Function.prototype.bind= function(context) {
        if (typeof this !== 'function') {
            throw new TypeError(`${this} is not a function!`);
        }
        let outerArgs = Array.prototype.slice.call(arguments, 1);
        // 这里也可以简写为 :
        // var outerArgs = [].slice.call(arguments, 1);
        let self = this;
        let F = function() {};
        let Bound = function() {
            let innerArgs = [].slice.call(arguments);
            let args = outerArgs.concat(innerArgs);
            return self.apply(this instanceof Bound ? this : context, args);
        };
        if (self.prototype) {
            F.prototype = self.prototype;
        }
        Bound.prototype = new F();
        return Bound;
    }
}

自定义 call 和 apply

自定义实现 call 方法

  Function.prototype.myCall = function(context) {
        context = context ? context : window; 
        //如果 context 为 null / undefined ,则指向window;
        context.fn = this;
        var args = new Array(arguments);
        args.splice(0, 1);
        var res = context.fn(...args); 
        //删除该方法,不然会对传入对象造成污染(添加该方法)
        delete context.fn;
        //原理:如果函数是在某个上下文对象中调用的,this 绑定的是那个上下文对象
        return res;
      }

自定义实现 apply 方法

Function.prototype.myApply - function(context) {
        context = context ? context : window;
        context.fn = this;
        var args = arguments[1];
        context.fn(...args);
      }

你可能感兴趣的:(bind / call / apply 用法整理 以及 自定义实现)