js call、apply 和 bind 方法的深入理解

目录

一、概述

二、详解 call()、apply() 和 bind() 方法

1、apply() 方法

2、call() 方法

3、bind() 方法

三、call()、apply() 和 bind() 方法的核心理念

四、手写 call()、apply() 和 bind() 方法(中高级前端面试题)★★★★★

1、手写 call() 方法

2、手写 apply() 方法

3、手写 bind() 方法

五、call()、apply() 和 bind() 方法的实际应用

1、如何选用 call()、apply() 和 bind() 方法?

2、应用实例

(1)、借用 Object.prototype.toString() 方法判断数据类型

(2)、借用数组的方法,操作类数组

(3)、用 apply() 方法获取数组的最大值和最小值

(4)、继承

(5)、保存函数参数(变量)

(6)、回调函数 this 丢失问题


参考:https://blog.csdn.net/mChales_Liu/article/details/103201171#6%E3%80%81%E5%87%BD%E6%95%B0%E5%86%85%E7%BD%AE%E6%96%B9%E6%B3%95

 

一、概述

  1. call()、apply() 和 bind() 方法都是用来改变函数执行时的上下文(this),可借助它们实现继承。
  2. 调用 call()、apply() 和 bind() 方法 的必须是个函数。
  • apply() 有两个参数,第一个参数是对象,第二个参数是数组,fn.apply(thisobj, arg),此处 arg 是一个数组。用来改变函数的执行环境。
  • call() 有 N 个参数,第一个参数都是对象,call 参数将依次传递给借用的方法作为参数,即 fn.call(thisobj, arg1, arg2)。用途和 apply() 完全一样,用来改变函数的执行环境。
  • bind() 会返回执行上下文被改变的函数,而不会立即执行,前两者时立即执行,参数和 call() 相同。

 

二、详解 call()、apply() 和 bind() 方法

1、apply() 方法

apply() 方法接收2个参数:运行函数的作用域(也叫上下文执行环境) 和 一个参数数组。

apply() 方法,除了可以用来改变函数的作用域,还可以用于将数组转为函数的参数。

function sum(num1, num2){
    return num1 + num2;
}
 
function fn1(num1, num2){
    return sum.apply(this, arguments);
}
 
function fn2(num1, num2){
    return sum.apply(this, [num1, num2]);
}
 
console.log(fn1(10, 10));                    // 20
console.log(fn2(10, 10));                    // 20

 由于arguments是类数组,所以arguments也可以这样。

2、call() 方法

call() 和 apply() 方法唯一区别是参数不一样,call() 方法是 apply() 方法的语法糖。

call() 方法也接收2个参数:运行函数的作用域(也叫上下文执行环境) 和 一个参数列表

function sum(num1, num2){
    return num1 + num2;
}
 
function fn(num1, num2){
    return sum.call(this, num1, num2);
}
 
console.log(fn(10, 10));                     // 20

call() 方法的另一种使用途径——将对象转为数组

// 将对象转为数组
let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

3、bind() 方法

bind() 方法与 call() 和 apply() 方法都可以改变上下文执行环境,不同的是,bind() 方法返回一个新函数,只有等这个新函数被调用才可以改变函数执行时的上下文(this),而 apply() 和 call() 方法是立即调用,立即就能改变 this 指向的。

bind() 方法接收2个参数:运行函数的作用域(也叫上下文执行环境)  和 一个参数列表
浏览器 IE8 及其以下版本不支持 bind() 方法。

var color = "red";
var obj = {
    color: "blue"
}
function fn(){
    console.log(this.color);                     // blue
}
var objColor = fn.bind(obj);
objColor();

 

三、call()、apply() 和 bind() 方法的核心理念

“A对象有个方法,B对象因为某种原因也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?最好的方案当然是借用 A 对象的方法啦,既达到了目的,又节省了内存。”——这就是call()、apply() 和 bind() 方法的核心理念——借用方法

 

四、手写 call()、apply() 和 bind() 方法(中高级前端面试题)★★★★★

1、手写 call() 方法

  • 根据 call() 方法的规则设置上下文对象,也就是 this 的指向。
  • 通过设置 context 的属性,将函数的this指向隐式绑定到 context 上。
  • 通过隐式绑定执行函数并传递参数。
  • 删除临时属性,返回函数执行结果。
Function.prototype.myCall = function (context, ...arr) {
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
    context[specialPrototype] = this; // 函数的this指向隐式绑定到context上
    let result = context[specialPrototype](...arr); // 通过隐式绑定执行函数并传递参数
    delete context[specialPrototype]; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};

2、手写 apply() 方法

  • 传递给函数的参数处理,不太一样,其他部分跟call一样。
  • apply() 方法接受第二个参数为类数组对象, 这里用了 JavaScrip t权威指南中判断是否为类数组对象的方法。
Function.prototype.myApply = function (context) {
    if (context === null || context === undefined) {
        context = window // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    // JavaScript权威指南判断是否为类数组对象
    function isArrayLike(o) {
        if (o &&                                    // o不是null、undefined等
            typeof o === 'object' &&                // o是对象
            isFinite(o.length) &&                   // o.length是有限数值
            o.length >= 0 &&                        // o.length为非负值
            o.length === Math.floor(o.length) &&    // o.length是整数
            o.length < 4294967296){                  // o.length < 2^32
            return true;
        } else {
            return false;
        }
    }
    const specialPrototype = Symbol('特殊属性Symbol'); // 用于临时储存函数
    context[specialPrototype] = this; // 隐式绑定this指向到context上
    let args = arguments[1]; // 获取参数数组
    let result;
    // 处理传进来的第二个参数
    if (args) {
        // 是否传递第二个参数
        if (!Array.isArray(args) && !isArrayLike(args)) {
            throw new TypeError('myApply 第二个参数不为数组并且不为类数组对象抛出错误');
        } else {
            args = Array.from(args) // 转为数组
            result = context[specialPrototype](...args); // 执行函数并展开数组,传递函数参数
        }
    } else {
        result = context[specialPrototype](); // 执行函数 
    }
    delete context[specialPrototype]; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};

3、手写 bind() 方法

  • 拷贝源函数:
  • 通过变量储存源函数
  • 使用 Object.create() 方法复制源函数的 prototype 属性给 fToBind
  • 返回拷贝的函数
  • 调用拷贝的函数:
  • new 调用判断:通过 instanceof() 方法判断函数是否通过 new 调用,来决定绑定的 context
  • 绑定 this+ 传递参数
  • 返回源函数的执行结果
Function.prototype.myBind = function (objThis, ...params) {
    const thisFn = this; // 存储源函数以及上方的params(函数参数)
    // 对返回的函数 secondParams 二次传参
    let fToBind = function (...secondParams) {
        console.log('secondParams',secondParams,...secondParams)
        const isNew = this instanceof fToBind // this是否是fToBind的实例 也就是返回的fToBind是否通过new调用
        const context = isNew ? this : Object(objThis) // new调用就绑定到this上,否则就绑定到传入的objThis上
        return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果
    };
    fToBind.prototype = Object.create(thisFn.prototype); // 复制源函数的prototype给fToBind
    return fToBind; // 返回拷贝的函数
};

 

五、call()、apply() 和 bind() 方法的实际应用

1、如何选用 call()、apply() 和 bind() 方法?

  • 如果不需要关心具体有多少参数被传入函数,选用apply()。
  • 如果确定函数可接收多少个参数,并且想一目了然表达形参和实参的对应关系,用call()。
  • 如果我们想要将来再调用方法,不需立即得到函数返回结果,则使用bind()。

2、应用实例

(1)、借用 Object.prototype.toString() 方法判断数据类型

用 Object.prototype.toString() 方法来判断类型再合适不过,借用这个方法,我们几乎可以判断所有类型的数据:

function isType(data, type) {
    const typeObj = {
        '[object String]': 'string',
        '[object Number]': 'number',
        '[object Boolean]': 'boolean',
        '[object Null]': 'null',
        '[object Undefined]': 'undefined',
        '[object Object]': 'object',
        '[object Array]': 'array',
        '[object Function]': 'function',
        '[object Date]': 'date', // Object.prototype.toString.call(new Date())
        '[object RegExp]': 'regExp',
        '[object Map]': 'map',
        '[object Set]': 'set',
        '[object HTMLDivElement]': 'dom', // document.querySelector('#app')
        '[object WeakMap]': 'weakMap',
        '[object Window]': 'window',  // Object.prototype.toString.call(window)
        '[object Error]': 'error', // new Error('1')
        '[object Arguments]': 'arguments',
    }
    let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型
    let typeName = typeObj[name] || '未知类型' // 匹配数据类型
    return typeName === type // 判断该数据类型是否为传入的类型
}
console.log(
    isType({}, 'object'), // true
    isType([], 'array'), // true
    isType(new Date(), 'object'), // false
    isType(new Date(), 'date'), // true
)

(2)、借用数组的方法,操作类数组

类数组因为不是真正的数组所有没有数组类型上自带的种种方法,所以我们需要去借用数组的方法。

比如借用数组的 push() 方法:

var arrayLike = {
  0: 'OB',
  1: 'Koro1',
  length: 2
}
Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2');
console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}

(3)、用 apply() 方法获取数组的最大值和最小值

apply() 方法直接传递数组做要调用方法的参数,也省一步展开数组。

比如使用 Math.max() 和 Math.min() 方法来获取数组的最大值和最小值:

const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6

(4)、继承

ES5的继承,都是通过借用父类的构造函数,来实现父类的方法和属性的:

// 父类
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; // 复杂类型
}
supFather.prototype.sayName = function (age) {
    console.log(this.name, 'age');
};
// 子类
function sub(name, age) {
    // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
    supFather.call(this, name);
    this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
    sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
    console.log(this.age, 'foo');
};
// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18} 

(5)、保存函数参数(变量)

首先来看下一道经典的面试题:

for (var i = 1; i <= 5; i++) {
   setTimeout(function test() {
        console.log(i);                  // 依次输出:6 6 6 6 6
    }, 1000);
}

造成这个现象的原因是等到 setTimeout() 方法异步执行时,i 已经变成6了。 那么如何使他输出 1 2 3 4 5 呢?

解决办法有三:

①、 闭包保存变量

for (var i = 1; i <= 5; i++) {
    (function (i) {
        setTimeout(function () {
            console.log('闭包:', i);             // 依次输出:1 2 3 4 5
        }, 1000);
    }(i));
}

②、bind() 方法保存变量

for (var i = 1; i <= 5; i++) {
    // 缓存参数
    setTimeout(function (i) {
        console.log('bind', i)                // 依次输出:1 2 3 4 5
    }.bind(null, i), 1000);
}

③、用 let 声明变量,形成块级作用域,来间接达到保存变量的目的

for (let i = 1; i <= 5; i++) {
   setTimeout(function test() {
        console.log(i);                  // 依次输出:1 2 3 4 5
    }, 1000);
}

(6)、回调函数 this 丢失问题

问题再现:

class Page {
    constructor(callBack) {
        this.className = 'Page'
        this.MessageCallBack = callBack // 
        this.MessageCallBack('发给注册页面的信息') // 执行PageA的回调函数
    }
}
class PageA {
    constructor() {
        this.className = 'PageA'
        this.pageClass = new Page(this.handleMessage) // 注册页面 传递回调函数 问题在这里
    }
    // 与页面通信回调
    handleMessage(msg) {
        console.log('处理通信', this.className, msg) //  'Page' this指向错误
    }
}
new PageA()

 上述代码中,this.pageClass = new Page(this.handleMessage) 这里 this 的指向发生了改变,怎么保持 this 指向一致呢?

两种方案:

①、 bind() 方法:

this.pageClass = new Page(this.handleMessage.bind(this)); // 绑定回调函数的this指向

②、 箭头函数:

this.pageClass = new Page(() => this.handleMessage()); // 箭头函数绑定this指向

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值