回味基础:call、apply和bind那些事儿

时间:2020-9-9 作者:admin

通过上一篇 全面认识this指向接触到了call和apply,所以这篇文章就来深入了解一下call、apply以及bind。

本文的主要内容包括:

  • call、apply、bind的模拟实现
  • 使用场景

模拟实现

call的实现

Function.prototype.callFn = function(context) {
    var context = context || window; // this 参数可以传 null,视为指向 window
    context.fn = this;

    var args = [];
    // 传入的参数可以不确定,所以将除了第一个对象之后的参数作为fn的形参传入
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    // 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]
    var result = eval('context.fn(' + args +')');
    delete context.fn;
    return result;
};

<!--下面测试一下-->

var foo = {
    value: 1
};

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

bar.callFn(foo, 'armor', 18); 

<!--输出:-->
// name:  armor
// age: 18
// 1

apply的实现

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

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}
<!--下面测试一下-->

var foo = {
    value: 1
};

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

bar.applyFn(foo, ['armor', 18]); 

<!--输出:-->
// name:  armor
// age: 18
// 1

bind的实现

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

bind与apply和call的区别就是bind不会被立即调用,它返回的是一个函数。

首先是使用apply来改写一个简单版的,

Function.prototype.bindFn = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }
}

但是现在只能给bind传递一个参数,真正的bind函数是可以传递多个参数的,第一个参数是要绑定给调用它的函数的上下文,其他的参数将会作为预设参数传递给这个函数。

改进一下,传递多个参数:

Function.prototype.bindFn = function () {
    var self = this;
    var args = Array.prototype.slice.call(arguments);
    var context = args.splice(0,1)[0];
    return function () {
        return self.apply(context, args);
    }
}

// <!--下面测试一下-->

var foo = {
    value: 1
};

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

var test = bar.bindFn(foo, 'armor', 18);
test();

<!--输出:-->
// name:  armor
// age: 18
// 1

注意:bind还有一个特点就是,在使用bind后,得到的函数使用new操作符进行操作之后,这个结果的上下文并不受传递给bind的上下文影响

最后一版:

Function.prototype.bindFn = function(){
    var args = Array.prototype.slice.call(arguments);
    var context = args.splice(0,1)[0];
    var fn = this;
    var noop = function(){}
    var res =  function(){
        let rest = Array.prototype.slice.call(arguments);
        // 检测 new,如果当前函数的this指向的是构造函数中的this 则判定为new 操作
        return fn.apply(this instanceof noop ? this : context,args,rest)
    }
    // 把函数的原型保留下来,修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    if(this.prototype){
        noop.prototype = this.prototype;
    }
    res.prototype = new noop();
    return res;
}

使用场景

合并数组

var a = [1, 2, 3];
var b = [4, 5];

// 将第二个数组融合进第一个数组
// 相当于 a.push(4, 5);
Array.prototype.push.apply(a, b);

a;  // [1, 2, 3, 4, 5]

注意: 当第二个数组太大时不要使用这个方法来合并数组,因为一个函数能够接受的参数个数是有限制的。

获取数组中的最大值和最小值

由于JS的数组中并没有max方法,但是Math中有,所以可以借助于Math来绑定this实现这个功能:

var numbers = [5, 25 , 1 , -25]; 
Math.max.apply(Math, numbers);   //25    
Math.max.call(Math, 5, 25 , 1 , -25); //25

// ES6
Math.max.call(Math, ...numbers); // 25

使用Object.prototype.toString.call(obj)检测对象类型

在JavaScript里使用typeof判断数据类型,只能区分基本类型,要想区分对象、数组、函数,可以使用Object.prototype.toString.call(obj)来检测对象。由于 JavaScript 中一切都是对象,都继承自Object,所以可以调用Object的原型方法来检测类型。

例如:

Object.prototype.toString.call({name: "jerry"}); //[object Object]
Object.prototype.toString.call(function(){}); //[object Function]

类数组对象转数组

将类数组转换成数组的常用方法是Array.prototype.slice.call()

原理是 sliceArray-like对象通过下标操作放进了新的 Array 里面

补充:

当然,类数组转换成数组不止是slice这个方法,下面补充一下几个其他方法。

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)

实现继承

在我的另一篇 JS继承方式总结中,借用构造函数(经典继承)里使用了call来实现继承。

function Animal() {
    this.type = 'animal';
    this.skin = ['black', 'white', 'brown']
}

function Cat() {
    // 继承Animal
    Animal.call(this);
};

var cat1 = new Cat();
cat1.skin.push('gray');

var cat2 = new Cat();

console.log(cat1.skin); // [ 'black', 'white', 'brown', 'gray' ]
console.log(cat2.skin); // [ 'black', 'white', 'brown' ]

参考文章:

本文的主要内容包括:

  • call、apply、bind的模拟实现
  • 使用场景

模拟实现

call的实现

Function.prototype.callFn = function(context) {
    var context = context || window; // this 参数可以传 null,视为指向 window
    context.fn = this;

    var args = [];
    // 传入的参数可以不确定,所以将除了第一个对象之后的参数作为fn的形参传入
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    // 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]
    var result = eval('context.fn(' + args +')');
    delete context.fn;
    return result;
};

<!--下面测试一下-->

var foo = {
    value: 1
};

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

bar.callFn(foo, 'armor', 18); 

<!--输出:-->
// name:  armor
// age: 18
// 1

apply的实现

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

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}
<!--下面测试一下-->

var foo = {
    value: 1
};

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

bar.applyFn(foo, ['armor', 18]); 

<!--输出:-->
// name:  armor
// age: 18
// 1

bind的实现

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

bind与apply和call的区别就是bind不会被立即调用,它返回的是一个函数。

首先是使用apply来改写一个简单版的,

Function.prototype.bindFn = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }
}

但是现在只能给bind传递一个参数,真正的bind函数是可以传递多个参数的,第一个参数是要绑定给调用它的函数的上下文,其他的参数将会作为预设参数传递给这个函数。

改进一下,传递多个参数:

Function.prototype.bindFn = function () {
    var self = this;
    var args = Array.prototype.slice.call(arguments);
    var context = args.splice(0,1)[0];
    return function () {
        return self.apply(context, args);
    }
}

// <!--下面测试一下-->

var foo = {
    value: 1
};

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

var test = bar.bindFn(foo, 'armor', 18);
test();

<!--输出:-->
// name:  armor
// age: 18
// 1

注意:bind还有一个特点就是,在使用bind后,得到的函数使用new操作符进行操作之后,这个结果的上下文并不受传递给bind的上下文影响

最后一版:

Function.prototype.bindFn = function(){
    var args = Array.prototype.slice.call(arguments);
    var context = args.splice(0,1)[0];
    var fn = this;
    var noop = function(){}
    var res =  function(){
        let rest = Array.prototype.slice.call(arguments);
        // 检测 new,如果当前函数的this指向的是构造函数中的this 则判定为new 操作
        return fn.apply(this instanceof noop ? this : context,args,rest)
    }
    // 把函数的原型保留下来,修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    if(this.prototype){
        noop.prototype = this.prototype;
    }
    res.prototype = new noop();
    return res;
}

使用场景

合并数组

var a = [1, 2, 3];
var b = [4, 5];

// 将第二个数组融合进第一个数组
// 相当于 a.push(4, 5);
Array.prototype.push.apply(a, b);

a;  // [1, 2, 3, 4, 5]

注意: 当第二个数组太大时不要使用这个方法来合并数组,因为一个函数能够接受的参数个数是有限制的。

获取数组中的最大值和最小值

由于JS的数组中并没有max方法,但是Math中有,所以可以借助于Math来绑定this实现这个功能:

var numbers = [5, 25 , 1 , -25]; 
Math.max.apply(Math, numbers);   //25    
Math.max.call(Math, 5, 25 , 1 , -25); //25

// ES6
Math.max.call(Math, ...numbers); // 25

使用Object.prototype.toString.call(obj)检测对象类型

在JavaScript里使用typeof判断数据类型,只能区分基本类型,要想区分对象、数组、函数,可以使用Object.prototype.toString.call(obj)来检测对象。由于 JavaScript 中一切都是对象,都继承自Object,所以可以调用Object的原型方法来检测类型。

例如:

Object.prototype.toString.call({name: "jerry"}); //[object Object]
Object.prototype.toString.call(function(){}); //[object Function]

类数组对象转数组

将类数组转换成数组的常用方法是Array.prototype.slice.call()

原理是 sliceArray-like对象通过下标操作放进了新的 Array 里面

补充:

当然,类数组转换成数组不止是slice这个方法,下面补充一下几个其他方法。

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)

实现继承

在我的另一篇 JS继承方式总结中,借用构造函数(经典继承)里使用了call来实现继承。

function Animal() {
    this.type = 'animal';
    this.skin = ['black', 'white', 'brown']
}

function Cat() {
    // 继承Animal
    Animal.call(this);
};

var cat1 = new Cat();
cat1.skin.push('gray');

var cat2 = new Cat();

console.log(cat1.skin); // [ 'black', 'white', 'brown', 'gray' ]
console.log(cat2.skin); // [ 'black', 'white', 'brown' ]

参考文章:

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。