一. 概览
axios源码中until.js文件封装了大量的公共方法, 其封装思路和实现过程非常值得我们学习,这对于我们进一步深入掌握javascript很有帮助。
本篇文章用到的知识点主要有闭包,立即执行函数, 柯里化,递归等
二.源码解读(柯里化)
1. 类型判断
const toString = Object.prototype.toString;
// eslint-disable-next-line func-names
const kindOf = (cache => {
// eslint-disable-next-line func-names
return thing => {
const str = toString.call(thing);
return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
};
})(Object.create(null));
function kindOfTest(type) {
type = type.toLowerCase();
return function isKindOf(thing) {
return kindOf(thing) === type;
};
}
//使用
const isDate = kindOfTest('Date');
const isFile = kindOfTest('File');
const isDataFlag = isDate(new Date())
(1)用到了立即函数,立即函数只会在一开始执行一次,一般用于初始赋值使用;
(2) 之所以初始值为Object.create(null)是为了赋值一个原型为空的空对象,一个纯净的空对象,好处比如在for in方法时,就不需要筛选自有属性这步了;
(3) 原本该函数应该调用两个参数的,一个是要判断的值数据类型,一个是值本身,一般可以利用一个函数传两个参数这样的方法;在axios源码中利用柯里化的手段,即每个函数传一个参数,函数里面返回函数的形式。
(4)因为柯里化的实现离不开闭包,所以在有些场景下具有缓存的效果
2. 柯里化特点总结
(1) 参数复用
通用函数 解决了兼容性问题,但也会带来使用的不便,比如不同的应用场景需要传递多个不同的参数来解决问 题
有的时候同一种规则可能会反复使用(比如校验手机的参数),这就造成了代码的重复,利用柯里化就能够消除重复,达到复用参数的目的;
(2) 柯里化是闭包的一个典型应用,利用闭包形成了一个保存在内存中的作用域,把接收到的部分参数保存在这个作用域中,等待后续使用。并且返回一个新函数接收剩余参数;
(3) 提前返回、延迟执行;
(4) 降低适用范围,提高适用性, 提高代码可读性
比如还有其它类似的场景也经常可使用柯里化
function checkByRegExp(regExp) {
return function(str) {
return regExp.test(str)
}
}
const checkPhone = curryingCheckByRegExp(/^1\d{10}$/)
checkPhone('15152525634');
3. 拓展: 柯里化函数封装
* @description: 将函数柯里化的工具函数
* @param {Function} fn 待柯里化的函数
* @param {array} args 已经接收的参数列表
* @return {Function}
*/
const currying = function(fn, ...args) {
// fn需要的参数个数
const len = fn.length
// 返回一个函数接收剩余参数
return function (...params) {
// 拼接已经接收和新接收的参数列表
let _args = [...args, ...params]
// 如果已经接收的参数个数还不够,继续返回一个新函数接收剩余参数
if (_args.length < len) {
return currying.call(this, fn, ..._args)
}
// 参数全部接收完调用原函数
return fn.apply(this, _args)
}
}
三. 源码解读(bind,isPlainObject方法)
1. bind
export default function bind(fn, thisArg) {
return function wrap() {
return fn.apply(thisArg, arguments);
};
}
const instance = bind(Axios.prototype.request, context);
instance 相当于得到wrap函数,等同于处理过的fn函数本身,instance的执行相当于fn函数的执行
2. isPlainObject
isPlainObject() 函数用于判断指定参数是否是一个纯粹的对象,返回值为Boolean类型。 “纯粹的对象”,就是通过 { }、new Object()、Object.create(null) 创建的对象。 这个方法的作用是为了跟其他的 JavaScript对象如 null,数组,宿主对象(documents),DOM 等作区分,因为这些用 typeof 都会返回object。
function isPlainObject(val) {
if (kindOf(val) !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(val);
return prototype === null || prototype === Object.prototype;
}
1Object.getPrototypeOf(val): 获取val原型上的数据;为null,说明是由Object.create(null)创建,为Object.prototype说明是{}或者new Object()方法生成
四. 源码解读(forEach,extend)
1. forEach
function forEach(obj, fn, {allOwnKeys = false} = {}) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}
let i;
let l;
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}
if (isArray(obj)) {
// Iterate over array values
for (i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
const len = keys.length;
let key;
for (i = 0; i < len; i++) {
key = keys[i];
fn.call(null, obj[key], key, obj);
}
}
}
// 使用
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method,
url,
data: (config || {}).data
}));
};
});
forEach方法有三个参数:
- 第一个参数obj为所要循环的对象/数组;
- 第二个参数为执行的方法,该方法是使用时自定义的。该方法有三个参数,分别为遍历数组/对象子项,子项的key,遍历数组/对象本身;
- 第三个参数判断是否要遍历对象/数组的原型上的值
2. extend方法
function extend(a, b, thisArg, {allOwnKeys}= {}) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
}, {allOwnKeys});
return a;
}
此方法是在forEach方法上的拓展,将b的属性赋值到a上
五. 源码解读(merge方法)
function merge(/* obj1, obj2, obj3, ... */) {
const result = {};
function assignValue(val, key) {
if (isPlainObject(result[key]) && isPlainObject(val)) {
result[key] = merge(result[key], val);
} else if (isPlainObject(val)) {
result[key] = merge({}, val);
} else if (isArray(val)) {
result[key] = val.slice();
} else {
result[key] = val;
}
}
for (let i = 0, l = arguments.length; i < l; i++) {
arguments[i] && forEach(arguments[i], assignValue);
}
return result;
}
- 主要用于多个对象的合并,如有相同属性,后面参数值会覆盖之前参数,类似于ES6中的扩展运算符;
- 深层结构: 循环中的forEach方法中,是对象属性的深拷贝,是数组属性的浅拷贝,所以当处理数组对象时,merge方法会改变原数据
拓展(深拷贝)
function deepClone(source) {
if (typeof source !== 'object' || source == null) {
return source;
}
const target = Array.isArray(source) ? [] : {};
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = deepClone(source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
此次有用到递归,递归的特点有如下几点
- 重复调用自己,且必须要有终止条件;
- 其中一开始会从外到内为顺序调用自身函数,最内层的函数先调用完成,然后依次由内到外调用完成调用的自身函数,此为‘归’