目录
一、用法
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