✍️ Tangxt | ⏳ 2020-06-12 | 🏷️ JS 专题 |
箭头函数是无法通过call等方法来指定其里边所谓的 this
的,毕竟箭头函数不存在 this
……
简单来说,你这样做:
var f = () => {
console.log(this)
}
f.call('hi') // -> this -> window
// 全局this变量
this // -> window
题外话:
箭头函数是 Function
的实例,所以箭头函数也是可以使用 call
、 apply
等这样的方法的,只是我们无法像new一个普通函数那样,new一个箭头函数罢了,简单来说,箭头函数就是无法成为一个实例的类!它只能被当作跑一段代码的过程封装!
规律:
THIS5:基于
call/apply/bind
可以改变函数中this的指向(强行改变),当然,箭头函数除外!
Q:ES6写一个方法?
let obj = {
fn: function() {}
}
let obj1 = {
fn() {}
}
这两种写法是等价的,都是普通函数方法,注意这后者可不是箭头函数哈!
call/apply
相同点:
唯一区别:
执行函数,传递的参数方式有区别,call是一个个的传递,而apply则是把需要传递的参数放到数组中整体传递,如这样:
func.call([context],10,20)
func.apply([context],[10,20])
形式上来看,apply只管俩参数,而call则可以管很多个参数,但它们俩的代码执行效果是一样的!
如:
function func(x, y) {
console.log(this, x, y);
}
func.call("call: ", 1, 2); // -> String {"call: "} 1 2
// func.apply("apply: ", 1,2); // -> 如果你这样传,会报「Uncaught TypeError: CreateListFromArrayLike called on non-object」这样的错误
func.apply("apply: ", [1, 2]); // -> String {"apply: "} 1 2
call/apply
都是改变this的同时直接把函数执行了,而bind不是立即执行函数,属于预先改变this和传递一些内容 -> “柯理化”(大函数里边返回小函数,该小函数供外边使用)
为什么需要bind?
有这么一段代码:
let obj = {
fn(x, y) {
console.log(this, x, y);
},
};
setTimeout(obj.fn, 1000, 1, 2);
我们要确定 obj.fn
的 this
指向为 obj
,而不是 window
那么我们就不能用 call
和 apply
了,毕竟它们俩是立即执行的,所以这个时候我们就得用上 bind
了,而且也不需要为定时器追加参数了……
因此,可有:
setTimeout(obj.fn.bind(obj, 1, 2), 1000);
Q:重写bind?
~(function anonymous(proto) {
function bind(context) {
// context may be null or undefined
// 为啥不这样写 context = context || window? -> 如果传的是''和0等这样的falsy值呢?
if (context == undefined) {
context = window;
}
// 获取传递的实参集合
// 这算是一个小技巧吧,可谓方法借用……当然这可不是随意借用的,你这arguments可符合某种数据结构才行,如可遍历的……
var args = [].slice.call(arguments, 1);
// 需要最终执行的函数 -> 那个callback
var _this = this;
return function anonymous() {
// 为啥要有这行代码呢? -> 因为返回的这个函数有可能被传入参数,如事件对象:
// document.body.onclick = obj.fn.bind(window, 10, 20)
// document.body.onclick = anonymous -> 浏览器会传个事件对象给这个anonymous,所以你得找个容器接收一下
var amArg = [].slice.call(arguments, 0);
// 为啥要用apply? -> 因为args是个数组哈!一般都会把像ev这样的内置参数扔到数组的最后,真正的bind源码也是这样写的……
_this.apply(context, args.concat(amArg));
};
}
proto.bind = bind;
})(Function.prototype);
如果你不考虑内部传参的话,其实这bind的写法还可以更简单……即小函数不用搞个
amArg
哈!
代码测试:
let obj = {
fn(...args) {
console.log(this, args);
},
};
setTimeout(obj.fn, 1000, 1, 2);
setTimeout(obj.fn.bind(obj, 1, 2), 1000);
document.body.onclick = obj.fn.bind(obj, 1, 2);
结果:
用ES6姿势重写一下?
!(function(proto) {
//经过一些大佬们的测试:apply的性能不如call,<= 3 个参数用call,否则考虑用apply
function bind(context = window, ...args) {
return (...amArg) => this.call(context, ...args.concat(amArg));
}
proto.bind = bind;
})(Function.prototype);
一个bind一行代码就完事了(不考虑new的情况,毕竟从返回箭头函数这个侧面就可以看出了)……
代码测试:
let obj = {
fn(...args) {
console.log(this, args);
},
};
setTimeout(obj.fn, 1000, 1, 2); // -> window、[1,2]
setTimeout(obj.fn.bind(obj, 1, 2), 1000); // -> obj、[1,2]
document.body.onclick = obj.fn.bind(obj, 1, 2); // -> obj、[1, 2, MouseEvent]
这次测试让我回忆起,连续bind的那种情况,讲真,我是无法理解为啥连续bind之后,最终callback的执行,会把所有的预设的参数都给拿到!
把
obj.fn
当作是一个普通对象过来看待,即便它本质上是一个函数!但我们这样obj.fn.bind()
使用它,那么就可以把其当作是普通对象来看,而不是一个函数,不然,会有种奇怪的感觉! -> 一个对象调用它的bind
方法,返回了一个函数 -> 你看是不是很符合直觉? -> 如果你这样:「一个函数调用了它的bind
方法,返回了一个函数」 -> 是不是感到很奇怪?
function add(...args) {
console.log(args);
return args.reduce((x, y) => {
return x + y;
});
}
let res = add.bind(null, 1, 2).bind(null, 3, 4).bind(null, 5, 6)(); // -> [1, 2, 3, 4, 5, 6]
console.log(res); // -> 21
如果我不用去管bind的内部实现原理,单纯从使用上看的话,那么我不需要考虑连续bind会出现递归的效果,我只管最后执行的callback,我们对其传了6个参数,如果是事件触发执行的callback,那么还会有一个「事件对象参数」 -> 这就类似于
add.bind(null,1,2,3,4,5,6)
oradd.bind(null,...[1,2,3,4,5,6])
-> 我不知道是否可以把其理解为无中生有的递归现象!形象想想的话 -> 第一次bind我成为了爸爸(返回儿子) -> 第二次bind我成了爷爷(返回孙子) -> 第三次bind我成了太爷爷(返回曾孙)……
题外话:
关于展开运算符的使用:
function fn(...args) {
console.log(args)
}
let a = [1, 2, 3]
let b = [4, 5, 6]
// 展开运算符,把数组展开成一个个参数 -> 优先级level1
// API调用仔不想一个个传,于是用一个数组,然后展开之,这其实和形参是一一对应的
fn(...a.concat(b)) // -> [1, 2, 3, 4, 5, 6]
形参:
...args
-> 实参:...a.concat(b)
->args
:[1, 2, 3, 4, 5, 6]
原理:用到
xxx.fn()
这种姿势来指定this
值
context
是引用类型值简单重写call:
!(function(proto) {
function call(context = window, ...args) {
//=>必须保证context是引用类型
context.$fn = this;
let result = context.$fn(...args);
delete context.$fn;
return result;
}
proto.call = call;
})(Function.prototype);
测试:
function fn(a, b) {
console.log(this);
console.log(a, b);
}
fn.call({}, 1, 2); // -> {$fn: ƒ}
fn.call(1, 1, 2); // -> Uncaught TypeError: context.$fn is not a function
context
是啥类型的值有校验this值的类型的call:
!(function(proto) {
function call(context = window, ...args) {
// null的typoof也是object -> 所以单独拎出来校验!
context === null ? (context = window) : null;
let type = typeof context;
// 如果context不是null,不是[]、不是{}、也不是function,更不是symbol,那么这个context肯定是基本类型值啦!
if (type !== "object" && type !== "function" && type !== "symbol") {
//=>基本类型值
switch (type) {
case "number":
context = new Number(context);
break;
case "string":
context = new String(context);
break;
case "boolean":
context = new Boolean(context);
break;
}
}
context.$fn = this;
let result = context.$fn(...args);
// 当你对call传一个变量对象的时候,这才会有意义,如let o = {} -> fn.call(o) -> o -> {},但this -> { $fn: f }
// 原生的call方法,this还是 -> {}
// 说白了,我们写的这个call方法,不会污染o变量,但会污染this
delete context.$fn;
return result;
}
proto.call = call;
})(Function.prototype);
测试:
function fn(a, b) {
console.log(this);
console.log(a, b);
}
fn.call({}, 1, 2);
fn.call(1, 1, 2);
结果:
如果我这样呢?
fn.call(undefined, 1, 2); // -> this -> window
那么fn里边的 this
就是 window
啦! -> 这一点我是没有预料到的,我不知道 function call(context = window)
这样对 context
给默认值的操作,在面对传参是 undefined
的情况下,会选择 context
值为 window
,而对于 null
传参则不是 window
-> 对于你这个重写的 call
的第一个形参而言,你不传参或者给个 undefined
都会选择 window
这个默认值
总之,对于这种姿势: function call(context = window){}
,你调用call方法,不传参数或传 undefined
, context
的值都是 window
原理:call和apply只有一个区别——参数传多个还是用数组收拢 -> apply就是call的语法糖!
function apply(context = window, args) {
context.$fn = this;
// 用展开运算符,展开args这个数组就完事儿了
let result = context.$fn(...args);
delete context.$fn;
return result;
}
Q:为什么老师会讲bind/call/apply这些方法的源码重写呢?
因为很多大公司都会叫你用原生JS重写一些bind/call/apply -> 目的:通过这个考察你对 this
、面向对象以及柯里化函数的整个掌握情况……
~ function() {
function call(context) {
context = context || window;
let args = [].slice.call(arguments, 1),
result;
context.$fn = this;
result = context.$fn(...args);
delete context.$fn;
return result;
}
Function.prototype.call = call;
}();
function fn1() {
console.log(1);
}
function fn2() {
console.log(2);
}
fn1.call(fn2);
fn1.call.call(fn2);
Function.prototype.call(fn1);
Function.prototype.call.call(fn1);
改一下那个call方法:
!(function(proto) {
function call(context = window, ...args) {
//=>必须保证context是引用类型
context.$fn = this;
let result = context.$fn(...args);
delete context.$fn;
return result;
}
proto.call = call;
})(Function.prototype);
结果:
1、2、匿名空函数执行、1
做法:
规律:
fn1.call(fn2)
-> 执行fn1
fn1.call.call(fn2)
-> 执行fn2
(看call的第一个参数)注意点:
Function.prototype
是个匿名空函数 -> 这是特殊情况!context
来表示呢? -> 我想了想,我觉得对于普通函数而言,函数的执行都是交由某个对象来执行的,如非严格模式下,你全局这样: fn()
,那个这个 fn
函数是由 window
来执行的…… -> 注意,箭头函数可没有 this
,所以也就没有这个箭头函数是交由哪个对象来执行的了……Function
原型的方法时,请把此时的函数看作是一个普通对象,如 fn.bind()
-> 把 fn.bind
这个过程看成是个 ({}).bind
;函数执行的时候,那就把函数看成是个 function
,如 fn.bind()
-> fn.bind
-> xx
-> xx()
fn.call(obj,1,2)
看成是 obj.fn(1,2)
参数是数组(
[]
)还是散列(普通对象结构{}
)?
apply用的call可不是原生的call方法,而是apply自己内部重写的call方法!
在参数为3个(包含3)以内时,优先使用 call 方法进行事件的处理。而当参数过多(多于3个)时,才考虑使用 apply 方法。
题外话,关于API的选择问题:
js call和apply的性能有差别吗? - 贺师俊的回答 - 知乎:
既然有两个完成相同事情的操作,但却长得不同的api,就是为不同的需求准备的。
所以请按照你的需求使用,而不是为了所谓性能强行转换成另一种。
要知道如果你自己可以通过转换获得性能提升,没有理由引擎不能做这种优化,只是早晚的事情。
最后,在最新的引擎上两者性能是基本一样的。
➹:call 和 apply 的区别是什么,哪个性能更好一些? - 掘金
➹:jsPerf: JavaScript performance playground
// 为了让每一个函数都可以调取这个方法了
Function.prototype.changeThis = function changeThis(context, ...args) {
// THIS:当前要执行并且改变THIS指向的函数
// CONTEXT特殊情况的处理:不传递是window,传递null/undefined也让其是window,传递非对象或者函数类型值,我们需要让其变为对象或者函数
context == null ? context = window : null;
//=> 为了过滤出传入参数是基本类型的情况
if (typeof context !== "object" && typeof context !== "function") {
//=> 运用构造函数常见一个基本数据类型的实例;
//=> 目的是为了在下面context[uniqueKey] 时,由于基本数据类型不能运用对象的“点”或者“[]”存储属性时报错的问题
context = new context.constructor(context);
}
//=> 利用模版字符串和时间戳生成一个唯一的属性名
let uniqueKey = `$$${new Date().getTime()}` ;
//=> 给参数中新增个uniqueKey属性与调用的函数关联
context[uniqueKey] = this;
//=> 让调用的函数执行
let result = context[uniqueKey](...args);
//=> 删除新增的属性
delete context[uniqueKey];
//=> 把函数执行的结果 return 出去
return result;
};