前言:呜呜呜,春招开始了,但是俺的代码内功还不够。在博主的推荐下,我定下了看源码的目标,并从axios开始努力!在阅读的过程中,我发现源码对于硬绑定的封装,巧的是,硬绑定恰好是看过的《你所不知道的JavaScript(上卷)》的重点内容,因此写下此文。
硬绑定
什么是硬绑定?在《你所不知道的JavaScript》中对this的指向做了非常全面的分析,并对多种绑定的优先级作出比较,硬绑定是其中的一种。
我所理解的硬绑定:通过显式手段对函数(对象)中的this进行强制改变,不管怎么调用(再次硬绑定除外)其中的this都指向硬绑定所带来的this。
apply(thisObj,[...argument])
使用
apply的第一个参数是Function,但是函数的其他参数将会被封装成数组形式进行传递。
let changeObj={
name:"changeObj",
showName:function (){
console.log(arguments);
console.log("函数内name"+this.name);
}
}
let zs={
name:"zs"
}
changeObj.showName(1,1,["a"])
console.log("changeObj.name"+changeObj.name);
console.log("apply调用");
changeObj.showName.apply(zs,[1,1,["a"]])
console.log("changeObj.name"+changeObj.name);
打印:
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namechangeObj
changeObj.namechangeObj
apply调用
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
changeObj.namechangeObj
解释:开始的时候,函数内部的this指向外层的changeObj,但是经过apply处理,函数中的this指向zs
实现
/**
* 方式一:很多实现都是用eval进行传参,但是俺觉得俺这样都行,没有发现啥问题
* ps :我知道了,方式一这样结构赋值是es6以上的,可能兼容性问题
*/
Function.prototype.myApplyNew=function(context,args){
// 确定上下文,如果context是null,
context = context || window || global
// 绑定当前函数
context.fn = this
if(Array.isArray(args)){
// 传参
let result = context.fn(...args)
// 原来的上下文并没有该函数
delete context.fu
// 返回执行的结果
return result
}else{
delete context.fu
throw "要传入数组捏,小兄弟"
}
}
// 方式二:
Function.prototype.myApplyOld=function(context,args){
context = context || window || global
context.fn = this
let argArr= [],result
console.log(args);
for(let i=0;i<args.length;i++){
argArr.push(`args[${i}]`)
}
result = eval(`context.fn(${argArr})`)
delete context.fn
return result
}
call(thisObj,...arguments)
使用
call函数的第一个参数是Function将要指定的this对象,其他参数则是Function所需要的参数列表,与apply的差别仅是不需要对参数进行数组的封装。
let changeObj={
name:"changeObj",
showName:function (){
console.log(argument)
console.log("函数内name"+this.name);
}
}
let zs={
name:"zs"
}
changeObj.showName(1,1,["a"])
console.log("changeObj.name"+changeObj.name);
console.log("call调用");
changeObj.showName.call(zs,1,1,["a"])
console.log("changeObj.name"+changeObj.name)
打印输出:
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namechangeObj
changeObj.namechangeObj
call调用
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
changeObj.namechangeObj
解释:开始的时候,函数内部的this指向外层的changeObj,但是经过call处理,函数中的this指向zs
实现
根据上文中的call函数的特点,我们可以手写自己的call
Function.prototype.myCall=function(context,...args){
// 确定上下文,如果context是null,
context = context || window || global
// 绑定当前函数
context.fn = this
// 传参
let returnObj = context.fn(...args)
// 原来的上下文并没有该函数
delete context.fu
// 返回执行的结果
return returnObj
}
bind(thisObj,...argument)
使用
bind和前二者有明显的区别,bind将返回一个函数进行调用,如果需要进行参数的传递,则可以直接添加参数,得到函数后,如果需要继续添加参数,可以在返回的函数中添加。
let changeObj={
name:"changeObj",
showName:function (){
console.log(arguments);
console.log("函数内name"+this.name);
}
}
let zs={
name:"zs"
}
changeObj.showName(1,1,["a"])
console.log("changeObj.name"+changeObj.name);
console.log("bind调用");
changeObj.showName.bind(zs)(1,1,["a"])
console.log("changeObj.name"+changeObj.name);
console.log("第二种调用方式");
changeObj.showName.bind(zs,1,1,["a"])()
console.log("继续添加参数");
changeObj.showName.bind(zs,1,1,["a"])(1)
打印:
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namechangeObj
changeObj.namechangeObj
bind调用
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
changeObj.namechangeObj
第二种调用方式
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
继续添加参数
Arguments(4) [1, 1, Array(1), 1, callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
解释:在bind执行的时候,已经对this进行改变,和前二者不同,他返回一个硬编码的新函数进行供给调用(其上下文指向bind绑定的this)
实现
Function.prototype.Mybind=function (context,...args){
context = context || window || global
const that = this
// 允许传入新增的
return function(...addArgs){
return that.call(context, ...[...args,...addArgs])
}
}
Object.prototype.hasOwnProperty.call()
其实严格来说本段只能算是call方法的一个另类运用,但是其出现在axios源码中,且在js中也是一个细节,因此抽出进行描述。
// 一般来说,当验证对象是否有某个属性的时候,可以直接对象.hasOwnProperty(属性)
let a={
name:"zs"
}
console.log(a.hasOwnProperty("name")); // true
// 但是假如对象重写了hasOwnProperty方法,将直接从对象中获取而非原型链上,则不能有效监测
// 此时如果使用Object.prototype.hasOwnProperty.call()进行绑定和验证,才能有效监测
let a={
name:"zs",
hasOwnProperty:()=>false
}
console.log(a.hasOwnProperty("name")); // false
console.log(Object.hasOwnProperty.call(a,"name")); // true
axios中的call和apply
为啥会注意到硬绑定呢?跟前文的Promise一样,也是从axios中知道的。其实axios值得我们初级程序员来说并不只有他发请求、适配器等功能,他的某些工具的实现也是值得学习的。
axios的一处call:axios工具类中的forEach()
下面的一段代码出自axios的utils文件中的forEeach
函数,该函数主要是用于遍历函数的执行,对函数中的this进行重置(指向全局对象,浏览器:window,node:global)并遍历参数进行执行。
/**
* Iterate over an Array or an Object invoking a function for each item.
*
* If `obj` is an Array callback will be called passing
* the value, index, and complete array for each item.
*
* If 'obj' is an Object callback will be called passing
* the value, key, and complete object for each property.
*
* @param {Object|Array} obj The object to iterate,
* @param {Function} fn The callback to invoke for each item
*/
function forEach(obj, fn) {
// 判空
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}
// 进行装载
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}
// 不管是下面的哪一行call都是对函数的触发和函数中的this的清空
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
这个封装函数用的是真的多,但是我认为最容易理解且最经典的还是对Axios的请求方法的生成。
axios中的一处apply
如果你用过axios.create(config)
,那你一定间接使用过apply。是用来把配置好的上下文对象赋值给request方法。
这一图的代码出自axios包/lib/axios.js是入口文件
下图是bind的代码,主要是上下文对象的硬绑定和传参:
结语
上面就是apply、call、bind的复习和一些使用,果然,经典的东西够吃一壶!
智者不入爱河,寡王一路offer!
我是momo,我们下期见!
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情