你活得不快乐的原因是:既无法忍受目前的状态,又没能力改变这一切。
哈哈,认真起来的样子,很有魅力嘛 ☺️☺️☺️
Bug 虐我千百遍,我待 Bug 如初恋。大家好,我是 Guyal,点个关注不迷路,小谷儿带你上高速;Skr~😉
在疫情期间,由于面试找工作被虐的体无完肤,所以下定决心,沉淀自己,等待蓄势待发;
我的目标是:吊打面试官...🙂
读完本篇文章,可以让你轻松手写实现自认为「比较优秀」的 CALL/APPLY 方法
前言
Function.prototype:call/apply/bind 都是用来改变this指向的,以后函数执行调用这三个方法就可以实现this的改变
CALL
的内部机制
插一波基础语法
定义
: 使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数
语法
:function.call(thisArg, arg1, arg2...)
作用
:
- 立即执行函数
- 改变函数中的
this
,并且给函数传递参数信息 call
方法是Function.prototype
上的方法
调用 call
方法,实则调用的是 Function.prototype
上的方法
Function.prototype.call = function call(context) {}
来个EG
// Function.prototype.call = function call(context) {}
function fn(x, y) {
console.log(this, x, y);// {name: "guyal_"} undefined undefined
};
let obj = {
name: 'guyal_'
};
obj
和 函数 fn
之间没有任何关联,所以无法基于 obj.fn
来改变 fn
中的 this
,所以会报错
obj.fn(); // 报错 obj.fn is not a function
call
方法来走一波
fn.call(obj);
得,输出的结果是我们想要的。call
方法帮我们解决了 this
的指向问题
{name: "guyal_"} undefined undefined
我们再来看 call
方法内部到底干了什么?
CALL
方法的执行顺序
实例基于
__proto__
找到Function.prototype
上的call
方法,把call
方法执行
console.log(fn.__proto__ === Function.prototype);// true
call
方法中的this
是fn
,「谁调用了call
方法,this
就指向谁」
Function.prototype.call = function call(context, ...params) {
console.log(this); // 输出函数 fn
}
context
存储的是传递的obj
Function.prototype.call = function call(context, ...params) {
console.log(this); // 输出 {name: "guyal_"},obj对象
}
👀 👀 👀
CALL 方法内部机制
- 把
fn
执行(也就是把call
中的this
执行) - 把
fn
中的this
改变为第一个参数context
(也就是obj
) - 在非严格模式下,
context
不传递或者传递null/undefined
,则this
都改为window
;严格模式下,不传是undefined
,否则传递谁,this
就改为谁
fn.call(obj);
fn.call(obj, 10, 20);
fn.call(null);
fn.call();
在严格模式下输出结果
在非严格模式下输出结果
call
的第一个参数是context
,也就是执行fn
, 把fn
中的this
变为10
, 传递参数为20
this
是Object
类型,它会把值类型10
自动转换为对象类型10
,即Number{10}
fn.call(10, 20); // this: Number {10} x: 20 y: undefined
apply
和 call
的唯一区别,就是在给 fn
传递参数的时候,apply
需要把所有需要传递的参数信息放在一个数组中,而 call
是一个个的传递进来,不论哪种方法的,最后的结果都是把这些参数一个个的传递给 fn
fn.apply(obj, [10, 20]);
重写 CALL
实现思路
function fn(x, y) {
console.log(this, x, y);
};
let obj = {
name: 'guyal_'
};
Function.prototype.call = function call(context) {}
- 把需要执行的函数和需要改变的
THIS
关联在一起
实现关联:在对象中添加一个属性,属性值就是当前要执行的函数fn
context
:传递的对象
this
:需要执行的函数
Function.prototype.call = function call(context) {
context.fn = this;
}
// 相当于
obj.fn = fn;
- 执行
obj.fn()
输出的结果是:{name: "guyal_", fn: ƒ} undefined undefined
,最终结果是OK
的
Function.prototype.call = function call(context) {
context.fn = this;
context.fn();
}
那么此时obj
多了一个 fn
属性,我们得需要手动移除自己新增的属性
- 执行完之后移除自己新增的属性
Function.prototype.call = function call(context) {
context.fn = this;
context.fn();
delete context.fn;
}
- 返回结果
Function.prototype.call = function call(context, ...params) {
context.fn = this;
var result = context.fn();
delete context.fn;
return result;
}
最终结果
浏览器控制台中输出的结果是函数执行的时候输出的,点击展开的结果是当前对内存中最新的结果
基本功能已实现。到最后的优化阶段~ 😉😉😉
- 处理传递的参数:
context
不传(undefined
)或者为null
console.log(null == undefined);// true
context == null ? context = window : null;
- 只有函数和对象才可以设置属性和方法,必须保证传递的第一个参数
context
是函数或者对象
匹配typeof
值为object
或者function
,如果不是的话,使用Object(context)
方法转换为复杂数据类型的值,否则就是null
!/^(object|function)$/.test(typeof context)?Object(context):null;
- 平时一般不会直接操作
this
,要用一个变量来接收this
,直接操作这个变量
let self = this;
- 新增的属性名必须保证唯一性,防止污染原始对象中的成员
let key = Symbol('KEY');
附上最终代码
Function.prototype.call = function call(context, ...params) {
let self = this,
result = null,
key = Symbol('KEY');
context == null ? context = window : null;
!/^(object|function)$/.test(typeof context)? Object(context) : null;
context.fn = self;
result = context.fn(...params);
delete context.fn;
return result;
}
function fn(x, y) {
console.log(this, x, y);
};
let obj = {
name: 'guyal_'
};
fn.call(obj, 10, 20);
CALL
与 APPLY
的区别:
call()
方法的作用和apply()
方法类似,区别就是call()
方法接受的是参数列表,而apply()
方法接受的是一个参数数组。call
的性能要比apply
好一些:尤其是传递三个及以上参数的时候
相同:call 和 apply 都是立即执行函数,并且改变函数中的 this,给函数传递参数信息
在这里不在详细分析,直接附上最终代码
Function.prototype.apply = function call(context, params) {
let self = this,
result = null,
key = Symbol('KEY'),
args = [];
context == null ? context = window : null;
!/^(object|function)$/.test(typeof context)? Object(context) : null;
if(Object.prototype.toString.call(params) !== '[object Array]' && typeof params !== 'undefined')throw new TypeError('CreateListFromArrayLike called on non-object');
context.fn = self;
if (!params) {
result = context.fn();
}else {
/*
for (var i = 0, len = params.length; i < len; i++) {
args.push('params[' + i + ']');
}
result = eval('context.fn(' + args + ')')
} */
result = context.fn(params);
delete context.fn;
return result;
}
function fn(x, y) {
console.log(this, x, y);
};
let obj = {
name: 'guyal_'
};
fn.apply(obj);
重写 BIND:柯理化函数(预处理思想)
bind:并不会把函数立即执行,它是预先处理函数中的THIS和参数的
把fn
方法本身作为值绑定给BODY
的点击事件,当触发BODY
的点击操作,浏览器会帮助我们把fn
函数执行「并且传递一个事件对象」,方法中的THIS
指向BODY
document.body.onclick = fn;
// this:body x:MouseEvent y:undefined
立即把fn
执行,把其执行的返回结果当作值,赋值给事件绑定,事件触发执行的是返回结果
document.body.onclick = fn();
假如我点击BODY
的时候,把FN
执行,并且让THIS
指向OBJ
,而且传递10/20
给X/Y
这样处理显然是不能实现的:因为call/apply都是把函数立即执行的,还没有等到点击的时候,函数都执行完了
document.body.onclick = fn.call(obj, 10, 20);
我们可以这样实现:
- 创建一个匿名函数,绑定给点击事件,触发BODY点击行为的时候,执行的是匿名函数
匿名函数中的this
指的值body
,ev
专递的是MouseEvent
事件对象;
创建匿名函数之后,我们就可以再匿名函数中,执行fn
,随意处理自己的逻辑
document.body.onclick = function (ev) {
// 在匿名函数中,我们自己执行fn即可,接下来想改啥改啥
fn.call(obj, 10, 20, ev);
};
bind
不会把函数立即执行,它是预先处理函数中的THIS
和参数
哟,巧了,你说这不是巧了嘛。我们想要实现的功能和bind
的作用一毛一样,看代码
document.body.onclick = fn.bind(obj, 10, 20);
重写 bind
代码实现
Function.prototype.bind = function bind(context, ...outerArgs) {
// this: fn
// context: obj
// ...outerArgs: [10, 20]
// 第一次执行函数只是把参数预存储, 第二次执行的是匿名函数, 把预先存储的参数拿过来, 使用 call 方法改变 this 的指向「柯理化函数,预处理思想」
let self = this;
return function (...innerArgs) {
self.call(context, ...outerArgs.concat(...innerArgs));
}
};
function fn(x, y, ev) {
console.log(this, x, y, ev);
};
let obj = {
name: 'guyal_'
};
document.body.onclick = function (ev) {
fn.call(obj, 10, 20, ev);
};
document.body.onclick = fn.bind(obj, 10, 20)
Ending
到这里本篇就可以结束了,如若有错误或者不严谨的地方,请多多提出。万分感谢~
Bug 虐我千百遍,我待 Bug 如初恋。大家再见,我是 Guyal,点个关注不迷路,小谷儿带你上高速;Skr~😉