call/apply和bind的使用和原理并手写实现
关于 this
承接上一篇文章JS中几种最基本的this情况分析,加入面向对象的情况,总结一下
关于 this
的几种情况
- 给当前元素的某个事件行为绑定方法,方法中的
this
是当前元素本身(排除:DOM2在IE6~8中基于attachEvent进行事件绑定,这样处理方法中的this
->window) - 方法执行,看方法前面是否有“点”,有“点”,“点”前面是谁
this
就是谁,没有“点”,this
就是window
(严格模式下是undefiend
)- 自执行函数中的
this
一般是window
/undefined
- 回调函数中的
this
一般是window
/undefined
(当然某些方法中会做一些特殊的处理) - 括号表达式有特殊性
- ......
- 自执行函数中的
new
执行方式构造函数,构造函数体中的this
是当前类的实例- 箭头函数中没有
this
(类似的还有块级上下文),所以无论怎样去执行,怎样去修改,都没有用,如果函数中出现this
,一定是其所在的上级上下文中的this
- 在
Function.prototype
提供了三个方法:call
/apply
/bind
,这三个方法都是用来强制改变函数中this
指向的(每一个函数都是Function
的实例,所以都可以调取这三个方法执行)
'use strict'
let obj = {
name: 'ttt',
fn() {
let self = this;
setTimeout(function () {
console.log(this);// this -> window
console.log(self);//self->obj
}, 1000);
setTimeout(() => {
console.log(this);
// this是上级上下文中的,也就是obj
}, 1000);
}
}
obj.fn();
call
/ apply
的使用和手写
call
想在fn执行的时候将其中的 this
转化为 obj
,并且传入其他参数
"use strict";
const fn = function fn(x,y){
console.log(this.name,x+y);
};
let obj = {
name: 'obj'
};
fn.call(obj,10,20)//'obj' 30
fn.call(obj,10,20)
运行如下:
fn
首先作为 Function
的实例,基于 __proto__
找到 Function.prototype.call
方法,并且把找到的 call
方法执行
call
方法执行时的一些步骤:
call
中的this
->fn
- 第一个参数
context
->obj
,是未来要改变的call
函数中的this
指向,让其指向第一个参数context
- 剩余的参数
params
->[10,20]
存储的是未来要给函数传递的实参
call
方法执行的作用:帮助我们把 fn
( call
中的 this
)执行,并且让方法中的 this
指向 obj
( context
),顺便把 10/20
( params
)传递给函数
注意点:
-
如果直接执行
fn.call()
一个值都不传的话,fn
中的this
非严格模式下是window
,严格模式下是undefined
。x
为undefined
,y
为undefined
-
如果执行
fn.call(null/undefined)
,非严格模式下this
是window
,严格模式下是null
/undefined
(即传什么就是什么) 。x
为undefined
,y
为undefined
总结:即严格模式下传入什么 this
就是什么,非严格模式下,传入 null
或者 undefined
this
是 window
例如执行 fn.call(10, 20)
: this
-> 10
, x
-> 20
, y
-> undefined
apply
apply
和 call
只有一个区别: call
方法在设定给函数传递的实参信息的时候,是要求一个个传递实参值。但是 apply
是要求用户把所有需要传递的实参信息以数组/类数组进行管理的。虽然要求以数组方式传进去,但是内部最后处理的时候,和 call
一样,也是一项项的传递给函数的。
fn.apply(obj, [10, 20])
所以一个函数创建出来的时候,其中的 this
直到执行的时候才可以确定。这时中, this
在不同的环境可以自动关联的值,也是可以我们使用 call
/ apply
/ bind
手动关联的值。
应用场景
场景一:求最大值或最小值
求一个数组中的最大值或者最小值
let arr = [10, 14, 23, 34, 20, 13];
-
排序处理 时间复杂度稍微高一些(
sort
内部处理的时间复杂度是 N^2,即遍历两次)console.log(arr.sort((a, b) => b - a)[0])
-
假设法 时间复杂度N,循环一次即可
let max = arr[0], i = 1, len = arr.length, item; for (; i < len; i++) { item = arr[i]; if (item > max) { max = item; } } console.log(max);
-
Math.max
获取最大值
console.log(Math.max(10, 14, 23, 34, 20, 13)); //=>34
console.log(Math.max(arr)); =>NaN
方法本身是传入的所有参数中的最大值,需要把比较的数字一项项的传递给 max
方法才可以
那么把数组中的每一项分别传递给 max
方法:
let max = Math.max(...arr);
console.log(max); =>34
let max = Math.max.apply(null, arr);
console.log(max);// =>34
利用 apply
的机制。虽然传递给 apply
的是一个数组,但是 apply
内部会把数组中的每一项分别传递给对应的函数。而且 Math.max
中用不到 this
,所以 this
改成谁无所谓,就是占个位而已
场景二:任意数求和
任意数求和(不确定实参的个数,所以无法基于设定形参接受)
两种方法:
- 剩余运算符 ES6
arguments
ES3
-
const sum = function sum(...params) { if (params.length === 0) return 0; //params -> 数组 return params.reduce((total, item) => total + item, 0); };
-
把类数组转换为数组
params = Array.from(params);
ES6+params = [...params];
ES6+- ...
那么如何使用es5以下方法将伪数组转换为数组?
最简单的方法:手动一个个转换
const sum = function sum() { let params = arguments; // params -> 类数组「不能直接使用数组的办法」 if (params.length === 0) return 0; let i = 0, len = params.length, arr = []; for (; i < len; i++) { arr[arr.length] = params[i]; <==> arr.push(params[i]); } return arr.reduce((total, item) => total + item, 0); };
借助
slice
:ary.slice() ary.slice(0)
以上都为数组的克隆,把原始数组中的每一项都克隆一份给返回的新数组(浅克隆)
解释一下为什么
[].slice.call(arguments)
会将伪数组集合变为真的数组MDN slice-polyfill
-
查看polyfill,
slice
原理是用了for...i
循环+往真是数组里循环存值,所以[].slice.call(arguments)
会把polyfill代码中的this
替换为arguments
,并且循环,然后往真的数组里按序存值,最后返回真的数组 -
因为类数组的结果和数组非常的相似(都有下标+
length
),所以大部分操作数组的代码,也同样适用于类数组,在这个前提下,我们只需要把实现好的数组方法执行,让方法中的this
变为类似组,就相当于类数组在操作这写代码,实现了类数组借用数组方法的目的,我们这种操作叫做“鸭子类型”
模拟
slice
原理:Array.prototype._slice = function slice() { //模拟 //this -> ary let i = 0, len = this.length, arr = []; for (; i < len; i++) { arr[arr.length] = this[i]; } return arr; };
把类数组转换为数组:如果我们能把
slice
执行,并且让slice
中的this
是arguments
,这样就相当于在迭代arguments
中的每一项,把每一项赋值给新的数组集合 -> 也就是把类数组转换为数组- 如何让
slice
执行 : 使用Array.prototype.slice()
/[].slice()
..... - 如何改变
slice
中的this
: 使用call/apply
->params = [].slice.call(params);
const sum = function sum() { let params = arguments; if (params.length === 0) return 0; params = [].slice.call(params); return params.reduce((total, item) => total + item, 0); };
或者不转换了,直接借用这种规则即可
const sum = function sum() { let params = arguments; if (params.length === 0) return 0; //不转换了,直接借用即可 return [].reduce.call(params, (total, item) => total + item, 0); };
或者直接修改原型链指向,这样调用的时候,里面的
this
就指向了arguments
const sum = function sum() { let params = arguments; //params.__proto__ = Array.prototype; 修改原型链的指向 arguments可以直接使用数组原型上的任何方法 //或者: params.reduce = Array.prototype.reduce; if (params.length === 0) return 0; return params.reduce((total, item) => total + item, 0); };
一个有趣的问题:
let obj = {
2: 3, //1
3: 4, //2
length: 2, //从2->3,从3->4
push: Array.prototype.push
};
obj.push(1)
obj.push(2)
console.log(obj[2])//1
console.log(obj[3])//2
console.log(obj.length)//4
原理是 push
中的 this
变为了 obj
那么push做了什么事?
手动模拟一下 push
(push是内置的):
Array.prototype.push = function push(val) {
//这些代码只是一些模拟
// this -> 操作的数组
this[this.length]=val;
this.length++;
};
arr.push(100);
把 push
方法执行,让里面的 this
-> obj
类似于
obj.push(1)
=> [].push.call(obj,1)
:
obj[obj.length]=1
=>obj[2]=1;
obj.length++
;
obj.push(2)
:
obj[obj.length]=2
=>obj[3]=2
obj.length++
手写 call
/ apply
用js模拟 call
面试题:完成change函数
~ function () {
/* 内置CALL实现原理 */
function change(context, ...params) {
};
Function.prototype.change = change;
}();
let obj = {
name: 'Alibaba'
};
function func(x, y) {
this.total = x + y;
return this;
}
let res = func.change(obj, 100, 200);
console.log(res);
//res => {name:'Alibaba',total:300}
实际上就是内置 call
的实现原理
call
的核心原理很简单:
obj.func = func
console.log(obj.func(100,200))
delete obj.func
拿上面例子举例就是让 obj
中有一个 func
属性,然后执行 obj.func
,那么其中的 this
就自动指向了 obj
,最后再删除,打印如下( console.log
打印的是最后运行代码,即删除完 func
的对象)
~ function () {
//在没有Symbol之前,我们可以用一个函数随机生成随机值来代替 Symbol('KEY')
/*const createRandom = () => {
let ran = Math.random() * new Date();
return ran.toString(16).replace('.', '');
};*/
/* 内置CALL实现原理 */
function change(context, ...params) {
// this->要执行的函数func context->要改变的函数中的this指向(当下例子中为obj)
// params->未来要传递给函数func的实参信息{数组} [100,200]
// 临时设置的属性,不能和原始对象冲突,所以我们属性采用唯一值处理
context == null ? context = window : null;
if (!/^(object|function)$/.test(typeof context)) context = Object(context);
let self = this,
key = Symbol('KEY'),
result;
context[key] = self;
result = context[key](...params);
delete context[key];
return result;
};
Function.prototype.change = change;
}();
change函数中
this
->要执行的函数func
context
->要改变的函数中的this
指向(当下例子中为obj
)params
->未来要传递给函数func
的实参信息{数组}[100,200]
要注意的是:
context
的要执行的那个属性是临时设置的属性,不能和原始对象冲突,所以我们属性采用唯一值Symbol
处理。- 传进来的
context
不一定是对象,只有对象设置属性才有用所以添加判断,如果传进来的值不是objec类型的,就要转换为对象if (!/^(object|function)$/.test(typeof context)) context = Object(context)
- 当传递的是
null
或者undefined
的时候,都应该将this
指向window
所以 :context == null ? context = window : null;
在没有 Symbol
之前,我们可以用一个函数随机生成 context
的 key
模拟 apply
apply和call唯一的区别就是,apply传入得失参数数组,
所以将上面calll实现的方法去掉 ...
不使用扩展运算符即可
function call(context, ...params) {}
function apply(context, params) {}
伪数组转化为真数组方法
小tips:将获取的参数转化为真正数组的方法:
function toArray(...params) {
return params;
}
function toArray() {
// arguments 类数组集合
// return [...arguments];
// return Array.from(arguments);
return [].slice.call(arguments);
}
bind
的使用与手写
举例子:
要求:
~function () {
//bind方法在ie6-8中不兼容,接下来我们自己基于原生js实现这个方法
function bind(context, ...params) {
}
Function.prototype.bind = bind;
}();
var obj = {name: 'ali'}
function func() {
console.log(this, arguments)
//当点击body 的时候,执行function方法,并且输出obj [100,200,MouseEvent事件对象]
}
document.body.onclick = func.bind(obj, 100, 200)
分析:
bind
& call
/ apply
区别:
- 都是为了改变函数中的
this
指向 call
/apply
:立即把函数执行bind
:不是立即把函数执行,只是预先把this
和后期需要传递的参数存储起来(预处理思想 -> 柯理化函数)
bind
的原理:其实就是利用闭包的机制,把要执行的函数外面包裹一层函数,预先把 this
和后期需要传递的参数存储起来。即柯理化思想的运用
事件相关分析:
function func(ev){
console.log(this,arguments)
}
document.body.onclick = func
浏览器在点击事件触发时,会自动绑定 this
为事件触发的DOM对象(这里是 body
)并且会默认传入事件对象作为参数
假设我们用内置的 bind
,看看结果如何:
var obj = {name: 'ali'}
function func() {
console.log(this, arguments)
}
document.body.onclick = func.bind(obj, 100, 200)
点击之后:
不仅把原来的参数穿进去,并且最后在末尾,还有传入的事件对象
题目最后目的:把 func
执行, this
改为 obj
,参数 100/200/ev
传递给他即可,如下:
document.body.onclick = function proxy(ev) {
// 最后的目的:把func执行,this改为obj,参数100/200/ev传递给他即可
func.call(obj, 100, 200, ev);
};
手写 bind
了解以上信息之后开始手写:
~ function () {
/* 内置BIND的实现原理 */
function bind(context, ...params) {
// this->func context->obj params->[100,200]
let self = this;
return function proxy(...args) {
// args->事件触发传递的信息,例如:[ev]
params = params.concat(args);
return self.call(context, ...params);
};
};
Function.prototype.bind = bind;
}();