call、apply、bind 作用和区别

目录

一、用法

1.1、call用法

1.2、apply用法 

1.3、bind用法

二、区别

2.1、相同点 

2.2、不同点

三、使用场景 

3.1 apply()的使用合并两个数组

3.1.1 原理

3.1.2 如何解决参数过多的问题呢?—— 将参数数组切块,循环传入目标方法

3.2 apply()、call() 获取数组中的最大值和最小值

3.3 call的使用 Object.prototype.toString() 验证是否是数组

3.4 类数组对象(Array-like Object)使用数组方法 

3.4.1 什么是类数组对象?为什么要出现类数组对象?

3.4.2 使用 Array.prototype.slice.call 将类数组对象转换为数组 

3.4.3 使用 ES6 提供的 Array.form / 解构赋值实现类数组对象转数组

3.5 call()调用父构造函数实现继承

3.6. bind() 使用场景


一、用法

call、apply、bind 都是函数 Function 原型上的方法,三者的功能都是用来改变函数中的 this 指向

 const ajie = {
            name: '阿杰'
        }

        const xiaoyu = {
            name: '小雨'
        }

        function hi(msg, msg2) {
            console.log(msg + msg2 + this.name);
        }
        hi.call(ajie, '你好啊', 'aa');//你好啊aa阿杰
        hi.apply(ajie, ['你好啊', 'yy']); // 你好啊yy阿杰
        hi.bind(xiaoyu, '哈哈', '原来是你啊')();//哈哈原来是你啊小雨
        //一般这样使用bind,把得到的函数保存下来
        const hixiaoyu = hi.bind(xiaoyu)
        hixiaoyu('哈哈', '终于等到你');//哈哈终于等到你小雨


        let o = {
            nick: '华晨',
            hi() {
                console.log(this.nick); 
            }
        }

        setTimeout(callback, 0); //此时运行会报错,因为当执行this.nick时cb前面没有.对象,this指向window
        //正确写法,要通过bind()改变this的指向
        setTimeout(o.hi.bind(o), 0); 
        function setTimeout(callback, ms) {
            cb();
        }


//经常有如下业务

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {
 
        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
//Hello, my name is Kitty
这里输出的nickname是全局的,并不是我们创建 person 时传入的参数,因为 setTimeout 在全局环境中执行,所以 this 指向的是window。

这边把 setTimeout 换成异步回调也是一样的,比如接口请求回调。

解决方案有下面两种。

解决方案1:缓存 this值
var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {
        
  var self = this; // added
        setTimeout(function(){
            console.log("Hello, my name is " + self.nickname); // changed
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil

解决方案2:使用 bind

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {
 
        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }.bind(this), 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil

1.1、call用法

call() 方法是预定义的 JavaScript 方法,它可以用来调用所有者对象作为参数的方法。通过 call(),您能够使用属于另一个对象的方法。

案列:

  const Person = {
    fullName: function () {
      return this.firstName + this.lastName
    }
  }
  const Person2 = {
    firstName: "哈",
    lastName: "嘿嘿",
  }
// 哈嘿嘿 此时fullName函数中this指向newPerson,通过call()来改变其中this的指向
  Person.fullName.call(Person2) 

 传参:

call传入的参数数量不固定,第一个参数代表函数内的this指向,从第二个参数开始往后,每个参数被依次传入函数。

  const Person1 = {
    fullName: function (country, city) {
      return this.firstName + this.lastName + " " + country + " " + city
    }
  }
  const Person2 = {
    firstName: "张",
    lastName: "三",
  }
  Person1.fullName.call(Person2, '中国', '河南') // 张三 中国 河南 

call是包装在apply上面的一颗语法糖,如果我们既明确知道函数接受参数的个数,又想清晰明了的表达形参和实参的对应关系,那么可以用call来传达参数。

1.2、apply用法 

apply接受两个参数,第一个参数指定了函数体内的this指向。第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数。

案列:

  const person = {
    fullName: function (country, city) {
      return this.firstName + this.lastName + " " + country + " " + city
    }
  }
  const newPerson = {
    firstName: "壮",
    lastName: "志国",
  }
  person.fullName.apply(newPerson, ['中国', '河南']) // 壮志国 中国 河南

代码中参数 ['中国', '河南'] 被放在数组中一起传给了person的fullName函数,分别对应fullName函数中的country,city参数。

当调用一个函数时,js的解释器并不会计较形参和实参在数量,类型以及顺序上的区别,js在内部就是用一个数组来表示的。从这个意义上来说,call比apply使用率更高,我们不必关心多少参数被传入函数,只要用apply一股脑推进去就行了。

1.3、bind用法


相信大家在使用React调用函数的时候必须使用bind(this),后直接在class中声明函数即可正常使用,但是为什么要使用这个呢?

bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值,例如:f.bind(obj),实际上可以理解为obj.f()这时f函数体内的this自然指向的是obj

  const person = {
    fullName: function (country, city) {
      return this.firstName + this.lastName + " " + country + " " + city
    }
  }
  const newPerson = {
    firstName: "壮",
    lastName: "志国",
  }
   // 打印出fullName函数
  person.fullName.bind(newPerson, '中国', '河南')() // 壮志国 中国 河南

bind传参和call是一致的,内部实现是先把当前函数保存起来,然后返回一个新函数,当我们将来要执行当前函数时,实际返回的是刚刚返回的新的fullName函数。它不会立即执行,而是需要的时候调用即可。

二、区别


2.1、相同点 


bind、call、apply都是用来指定一个函数内部的this的值。 
接收的第一个参数都是this要指向的对象。
都可以利用后续参数传参。


2.2、不同点


call和bind传参相同,多个参数依次传入的。
apply只有两个参数,第二个参数为数组。
call和apply都是对函数进行直接调用,而bind方法不会立即调用函数,而是返回一个修改this后的函数。

三、使用场景 


call函数的使用多用于类的继承。
apply函数可配合Math.max()用于计算数组最大值等。
bind函数可用于函数内部有定时器,改变定时器内部的this指向

3.1 apply()的使用合并两个数组


3.1.1 原理


使用 apply 将 Array.prototype.push 这个函数方法的 this 指向改成 arr1
也就是说:arr1 现在有一个 push 属性方法
又因为 apply 改变 this 指向后,会直接执行函数
所以 arr1 会直接调用 push 方法,并接收 arr2 传来的参数数组
最终实现数组合并
注意:

arr2 数组不能太大,因为一个函数能接受的参数个数有限,JavaScript 核心限制在 65535
不同引擎限制不同,如果参数太多,可能会报错,也可能不会报错但参数丢失


3.1.2 如何解决参数过多的问题呢?—— 将参数数组切块,循环传入目标方法


具体实现步骤:

定义每次连接的数组,最多有 groupNum 个元素
需要连接的数组 arr2 总长度设为 len
使用 for 循环,每循环一次,i 增加一个分组那么多
也就是说,每循环一次,就连接原数组 和 新数组的第 i 个分组
最后一个分组,如果元素不够,则直接截取到最后,也就是 arr2.length

function concatOfArray(arr1, arr2) {
  // 数组分组后,每组元素个数
  var groupNum = 32768;
  var len = arr2.length;
  // 每循环一次,数组都添加一组个数
  for (var i = 0; i < len; i += groupNum) {
    // 当最后一组个数不足 groupNum 时,直接截取到最后即可,也就是 len
    // 一块一块连接数组
    Array.prototype.push.apply(arr1, arr2.slice(i, Math.min(i + groupNum, len)));
  }
  return arr1;
}
 
// 验证代码
var arr1 = [-3, -2, -1];
var arr2 = [];
for (var i = 0; i < 1000000; i++) {
  arr2.push(i);
}
 
Array.prototype.push.apply(arr1, arr2);
// Uncaught RangeError: Maximum call stack size exceeded
 
concatOfArray(arr1, arr2);
// (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]


3.2 apply()、call() 获取数组中的最大值和最小值


数组没有直接获取最大最小值的方法,但是 Math 有
使用 call 将 Max.max 这个方法的 this 指向绑定到 Math 上
由于 call 会让绑定后的函数立刻执行,因此接收到数组后,Math 会立即执行寻找最值
 

var numbers = [5, 458 , 120 , -215 ]; 
 
Math.max.apply(Math, numbers); // 458    
 
Math.max.call(Math, 5, 458 , 120 , -215); // 458
 
// ES6
Math.max.call(Math, ...numbers); // 458


3.3 call的使用 Object.prototype.toString() 验证是否是数组


不同对象的 toString() 有不同的实现,可以通过 Object.prototype.toString() 获取每个对象的类型使用 call()、apply() 实现检测,下面是我在 chrome 中打印的效果

因此,可以这么封装:

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]';
}
 
isArray([1, 2, 3]); // true


3.4 类数组对象(Array-like Object)使用数组方法 


3.4.1 什么是类数组对象?为什么要出现类数组对象?


JavaScript 中有一种对象,结构非常像数组,但其实是个对象:

类数组对象不具有:push、shift、forEach、indexOf 等数组方法
类数组对象具有:指向对象元素的数字索引下标 和 length 属性
常见的类数组对象:

arguments 参数列表
DOM API 返回的 NodeList
类数组对象出现的原因:为了更快的操作复杂数据。

JavaScript 类型化数组是一种类似数组的对象,并提供了一种用于访问原始二进制数据的机制。Array存储的对象能动态增多和减少,并且可以存储任何 JavaScript 值。JavaScript 引擎会做一些内部优化,以便对数组的操作可以很快。然而,随着 Web 应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑,访问 WebSockets 的原始数据等,很明显有些时候如果使用 JavaScript 代码可以快速方便地通过类型化数组来操作原始的二进制数据,这将会非常有帮助。

3.4.2 使用 Array.prototype.slice.call 将类数组对象转换为数组 


slice 将 Array-like 类数组对象,通过下标操作,放进了新的 Array 里面:

将数组的 slice 方法,通过 call 改变 this 指向,绑定到需要修改的类数组对象;
由于 call 会在修改完绑定后自动执行函数,因此 类数组对象 调用它被绑的 slice 方法,并返回了真的数组
// 类数组对象不是数组,不能使用数组方法
var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a function
 
// 使用 Array.prototype.slice.call 将类数组对象转换成数组
var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1");
// ["h1", html.gr__hujiang_com, head, meta, ...] 
也可以这么写,简单点 —— var arr = [].slice.call(arguments);

注意:此方法存在兼容性问题,在 低版本IE(< 9) 下,不支持 Array.prototype.slice.call(args),因为低版本IE下的 DOM 对象,是以 com 对象的形式实现的,JavaScript 对象与 com 对象不能进行转换

3.4.3 使用 ES6 提供的 Array.form / 解构赋值实现类数组对象转数组


Array.from() 可以将两种 类对象 转为 真正的数组:

类数组对象(arguments、NodeList)
可遍历(iterable)对象(包括 ES6 新增的数据结构 Set 和 Map)
let arr = Array.from(arguments);
let arr = [...arguments];


3.5 call()调用父构造函数实现继承


在子构造函数中,通过调用父构造函数的 call()方法,实现继承

SubType 的每个实例都会将SuperType 中的 属性/方法 复制一份

function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // 核心代码,继承自SuperType
    SuperType.call(this);
}
 
var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]
 
var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]

缺点:

只能继承父类的实例属性和方法,不能继承原型属性/方法
无法实现复用,每个子类都有父类实例函数的副本,影响性能 


3.6. bind() 使用场景

可以通过toString() 来获取每个对象的类型,但是不同对象的 toString()有不同的实现,所以通过 Object.prototype.toString() 来检测,需要以 call() / apply() 的形式来调用,传递要检查的对象作为第一个参数。

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]';
}
function isNumber(obj) {
    return Object.prototype.toString.call(obj) === '[object Number]';
}

function isString(obj) {
    return Object.prototype.toString.call(obj) === '[object String]';
}
isArray([1, 2, 3]);
// true
 
// 直接使用 toString()
[1, 2, 3].toString();  // "1,2,3"
"123".toString();   // "123"
123.toString();   // SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).toString(); // "123"

  另一个验证是否是数组的方法,这个方案的优点是可以直接使用改造后的 toStr。 

var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){ 
    return toStr(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true
 
// 使用改造后的 toStr
toStr([1, 2, 3]);  // "[object Array]"
toStr("123");   // "[object String]"
toStr(123);   // "[object Number]"
toStr(Object(123)); // "[object Number]"

上面方法首先使用 Function.prototype.call函数指定一个 this 值,然后 .bind 返回一个新函数,始终将 Object.prototype.toString 设置为传入参数。其实等价于 Object.prototype.toString.call() 。

这里有一个前提toString()方法没有被覆盖

Object.prototype.toString = function() {
    return '';
}
isArray([1, 2, 3]);
// false
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值