目录
一、用法
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; // addedsetTimeout(function(){console.log("Hello, my name is " + self.nickname); // changed}, 500);}
}var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil解决方案2:使用 bindvar 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 exceededconcatOfArray(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