目录
1. call()、apply()、bind() 三者区别
1.1 作用
1.2 参数
1.3 执行时机
2. call()、apply() 使用场景
2.1 使用 Array.prototype.push.apply(arr1, arr2) 合并两个数组
2.1.1 原理(看了手写方法,或许会更有助于理解)
2.1.2 如何解决参数过多的问题呢?—— 将参数数组切块,循环传入目标方法
2.2 获取数组中的最大值和最小值
2.3 使用 Object.prototype.toString() 验证是否是数组
2.4 类数组对象(Array-like Object)使用数组方法
2.4.1 什么是类数组对象?为什么要出现类数组对象?
2.4.2 使用 Array.prototype.slice.call 将 类数组对象 转换为 数组
2.4.3 使用 ES6 提供的 Array.form / 解构赋值 实现 类数组对象 转 数组
2.5 调用父构造函数实现继承
3. bind() 使用场景
3.1 使用 Object.prototype.toString() 验证是否是数(bind 版)
3.2 柯里化(curry)
4. 手写 call()、apply()、bind()
4.1 实现 call()、apply()
4.1.1 实现思路
4.1.2 实现代码
4.1.3 优化版本(具体请阅读下方参考文章木易杨老师的博客,此处仅放结果)
4.2 实现 bind()
4.2.1 实现思路
4.2.2 实现代码
5. 参考链接
call()、apply()、bind() 都用于 显式绑定 函数的 this 指向
call()、apply()、bind() 第一个参数相同:都代表 this 要指向的对象(若该参数为 undefined 或 null 或 不传参,this 则默认指向全局 window)
call()、apply()、bind() 除第一个传参外的其他参数不同:
call()、apply() 是立即执行
bind() 是返回绑定 this 之后的新函数,需要手动调用;如果这个新函数作为 构造函数 被调用,那么 this 不再指向传入 bind() 的第一个参数,而是指向新生成的对象
再来回忆一遍这两位的区别:
var func = function(arg1, arg2) {
...
};
func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组
注意:
具体实现步骤:
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, ...]
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
不同对象的 toString() 有不同的实现,可以通过 Object.prototype.toString() 获取每个对象的类型
使用 call()、apply() 实现检测,下面是我在 chrome 中打印的效果
因此,可以这么封装:
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1, 2, 3]); // true
JavaScript 中有一种对象,结构非常像数组,但其实是个对象:
常见的类数组对象:
类数组对象出现的原因:为了更快的操作复杂数据。
JavaScript 类型化数组是一种类似数组的 对象,并提供了一种用于访问原始二进制数据的机制。Array存储的对象能动态增多和减少,并且可以存储任何 JavaScript 值。JavaScript 引擎会做一些内部优化,以便对数组的操作可以很快。然而,随着 Web 应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑,访问 WebSockets 的原始数据等,很明显有些时候如果使用 JavaScript 代码可以快速方便地通过类型化数组来操作原始的二进制数据,这将会非常有帮助。
slice 将 Array-like 类数组对象,通过下标操作,放进了新的 Array 里面:
// 类数组对象 不是数组,不能使用数组方法
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 对象不能进行转换
Array.from() 可以将两种 类对象 转为 真正的数组:
let arr = Array.from(arguments);
let arr = [...arguments];
在子构造函数中,通过调用父构造函数的 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"]
缺点:
再来回忆下 bind() 使用方法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind()
方法会创建一个新函数,当这个新函数被调用时,它的this
值是传递给bind()
的第一个参数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。bind 返回的绑定函数也能使用
new
操作符创建对象:这种行为就像把原函数当成构造器,提供的this
值被忽略,同时调用时的参数被提供给模拟函数。
bind() 是 ES5 加入的,IE8 以下的浏览器不支持
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]"
注意:如果 toString() 方法被覆盖了,则上述方法无法使用:
Object.prototype.toString = function() {
return '';
}
isArray([1, 2, 3]); // false
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
如下所示:
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
add(1)(2);
// 3
关键点:
因此得出以下思路:
由于 call()、apply() 仅接收值不同,此处仅用 call() 做例子
实现 apply(),只需要将 入参 ....args,替换为 args 即可(多个参数转换为一个数组参数)
/**
* 实现 call
* @param context 要将函数显式绑定到哪个对象上
* @param args 参数列表
*/
Function.prototype.Call = function (context, ...args) {
// context 为 undefined 或 null 或 不传参 时,则 this 默认指向全局 window
if (!context || context === null || context === undefined) {
context = window;
}
// 利用 Symbol 创建一个唯一的 key 值,防止新增加的属性与 context 中的属性名重复
let fn = Symbol();
// 把调用 Call 的函数,作为属性,赋给即将绑定的对象
// 比如 foo.Call(context),把 foo 作为属性,赋值给 context
context[fn] = this;
// Call 显示绑定后,函数会自动执行
// 因此此处调用 context 上新增的属性 fn,也就是 foo 方法
// 方法执行时,谁调用,就隐式绑定到谁身上,此处 foo 方法就被隐式绑定到了 context 上
let res = context[fn](...args);
// 执行完成后,删除新增加的 fn 属性
delete context[fn];
return res;
};
ES3 call:
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
ES6 call:
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
context.fn = this;
let args = [...arguments].slice(1);
let result = context.fn(...args);
delete context.fn
return result;
}
ES3 apply:
Function.prototype.apply = function (context, arr) {
context = 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;
}
// ES6 apply:
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}
delete context.fn
return result;
}
关键点:
/**
* 实现 bind
* @descripttion bind 要考虑返回的函数,作为 构造函数 被调用的情况
* @param context 要将函数显式绑定到哪个对象上
* @param args 参数列表
*/
Function.prototype.Bind = function (context, ...args) {
// context 为 undefined 或 null 或 不传参 时,则 this 默认指向全局 window
if (!context || context === null || context === undefined) {
context = window;
}
// 利用 Symbol 创建一个唯一的 key 值,防止新增加的属性与 context 中的属性名重复
let f = Symbol();
// 此处的 fn 表示调用 Bind 的函数,或者 函数新创建的对象
// 比如 foo.Bind(obj),this 就代表 foo
// 再比如 const a = new foo.Bind(obj)(); Bind 返回的函数作为构造函数使用,则 this 就代表新创建的对象 a
let fn = this;
const result = function (...args1) {
// this instanceof fn —— 用于判断 new 出来的对象是否是 fn 的实例
if (this instanceof fn) {
// result 如果作为构造函数被调用,this 指向的是 new 出来的对象
this[f] = fn;
let res = this[f](...args, ...args1);
// 执行完成后,删除新增加的属性
delete this[f];
return res;
} else {
// result 如果作为普通函数被调用,this 指向的是 context
context[f] = fn;
let res = context[f](...args, ...args1);
// 执行完成后,删除新增加的属性
delete context[f];
return res;
}
};
// 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法
// 使用 Object.create 实现继承
result.prototype = Object.create(fn.prototype);
// Bind 函数被调用后,返回一个新的函数,而不是直接执行
return result;
};
深度解析 call 和 apply 原理、使用场景及实现 | 木易杨前端进阶高级前端进阶之路https://muyiy.cn/blog/3/3.3.html#%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF深度解析bind原理、使用场景及模拟实现 | 木易杨前端进阶高级前端进阶之路
https://muyiy.cn/blog/3/3.4.html#bind