本文参加了由 公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第xx期,语雀链接: www.yuque.com/ruochuan12/…。
前言
最近因为一个会议,从公司领导那里听到了很多东西,感慨万千,想到我曾在樊登讲书听到的一本书 -- 思维的泥潭。
但这里要是展开说故事,那主题就不是 axios 工具函数了哈哈,虽说本人的经历十分的普通,但是扯起来,也会...
我就拿领导举过的一个例子来说,对于俄乌局势,有人支持俄罗斯,有人支持乌克兰,但如果我们站在旁观角度去分析为什么支持俄罗斯,为什么支持乌克兰,这就是一种思维打开的方式 -- 不能局限于一种思维方式。我们在日常生活中更容易依赖于自己固有的一套思维模式,所以会导致有时候我们把自己困在思维的囚笼里。
就比如我自己,我之前想法就是把工作和生活分的有点开,但如果我把这俩融合一点,我觉得,我对前端这行的理解,不会局限于它就是一种谋生方式。毕竟我就一个很简单的人,一边两眼放光地感叹好神奇的技术,一边感慨技术好多好难卷不动。
也就说这么多吧,希望大家多多指点~
准备工作
// 克隆源码 axios v1.4.0
git clone https://github.com/axios/axios.git
cd axios
找到入口文件 index.js :
import axios from './lib/axios.js';
主要文件 axios.js ,然后我们主要聚焦 utils.js 文件:
import utils from './utils.js';
工具函数
通用函数
首先是通用的高阶函数简单声明,比如 kindOf 、kindOfTest、typeOfTest 等(在 v0.27.0 开始这样写了),优雅而简洁~
const {toString} = Object.prototype;
const {getPrototypeOf} = Object;
const kindOf = (cache => thing => {
const str = toString.call(thing);
// 在判断数据的类型的时候,常会使用到slice(8,-1)表示从字符串第八位开始截取到倒数第一位(不含尾)
return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
})(Object.create(null));
// 例如 kindOf([1, 2]) => [Object Array] => 'array'
const kindOfTest = (type) => {
type = type.toLowerCase();
return (thing) => kindOf(thing) === type
}
const typeOfTest = type => thing => typeof thing === type;
// 确定值是否为 数组
const {isArray} = Array;
tips:
- 这里的 kindOf 是一个高阶函数,它返回一个新函数,这个新函数可以判断传入的参数 thing 的类型,并返回一个字符串表示类型,其实核心就是 Object.prototype.toString.call() 。不过kindOf 内部使用了一个 cache 对象来缓存已经处理过的类型,避免重复计算。
- kindOfTest 是另一个函数,它接受一个类型参数 type,然后返回一个新函数,这个新函数可以判断传入的参数 thing 是否为指定的类型。kindOfTest 内部调用了 kindOf 函数来获取 thing 的类型,并将其转换为小写字母,然后与传入的 type 进行比较。如果相同,返回 true,否则返回 false。
- 这里的 typeOfTest 是一个高阶函数,它接受一个参数 type,然后返回一个新的函数,这个新函数接受一个参数 thing,然后判断 thing 的类型是否等于 type,并返回布尔值。
例如,如果我们调用 const isString = typeOfTest('string'),那么 isString 将成为一个新函数,它可以接受一个参数 thing,并判断 thing 是否为字符串类型。这个新函数可以反复使用,只需要传入不同的 thing 值即可。
使用 typeOfTest 对数据类型进行判断
就相当于定义一个使用 typeof 判断数据类型的函数,这个大家都熟悉。
// 判断值是否为 'undefined' 类型
const isUndefined = typeOfTest('undefined');
// 判断值是否为 'string' 类型
const isString = typeOfTest('string');
// 判断值是否为 'function' 类型
const isFunction = typeOfTest('function');
// 判断值是否为 'number' 类型
const isNumber = typeOfTest('number');
isArray
判断是否是一个数组,这里是直接使用的 Array 内置的 isArray 方法。
const {isArray} = Array;
isBuffer
判断数据是否为 Buffer 对象。
对于 Buffer ,官方定义是 Buffer 对象用于表示固定长度的字节序列。Buffer 类是 JavaScript Uint8Array 类的子类,并使用涵盖额外用例的方法对其进行扩展。
JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。 但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。
function isBuffer(val) {
return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
&& isFunction(val.constructor.isBuffer) && val.constructor.isBuffer(val);
}
这是一长串判断:判断 val 是否为 null,是否为 undefined, val 的 constructor 属性是否为 null, val 的 constructor 属性是否为 undefined, val 的 constructor 属性是否有 isBuffer 方法,并且 isBuffer 方法是一个函数,调用 val 的 constructor.isBuffer 方法来判断 val 是否为一个 Buffer 对象,如果以上都是,则返回 true,否则返回 false。
isArrayBuffer
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。
MDN 的介绍是:它是一个字节数组,通常在其他语言中称为“byte array”。你不能直接操作 ArrayBuffer 中的内容;而是要通过类型化数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
// 确定值是否为 'ArrayBuffer' 类型
const isArrayBuffer = kindOfTest('ArrayBuffer');
// 相当于
(val) => kindOf(val) === 'arraybuffer'
isArrayBufferView
确定值是否是是一种 ArrayBuffer 视图(view)
function isArrayBufferView(val) {
let result;
if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) {
result = ArrayBuffer.isView(val);
} else {
result = (val) && (val.buffer) && (isArrayBuffer(val.buffer));
}
return result;
}
- 首先检查当前环境是否支持 ArrayBuffer,并且是否支持 ArrayBuffer.isView 方法。如果两个条件都成立,那么就调用 ArrayBuffer.isView 方法来判断传入的参数 val 是否是一个 ArrayBuffer 视图。最后将判断结果赋值给变量 result。
- 如果环境不支持 ArrayBuffer 或者不支持 ArrayBuffer.isView 方法,则通过判断值是否存在 buffer 属性,在通过上面一个函数 isArrayBuffer 对这个 buffer 值进行判断,将判断结果赋值给 result。
isDate
判断一个值是否为 Date 类型
const isDate = kindOfTest('Date');
isObject
判断一个值是否为 Object 类型
const isObject = (thing) => thing !== null && typeof thing === 'object';
在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 存储为全零,所以 typeof 将它错误的判断为 object 。在平时判断时可以先排除不是 null 再做判断。
isBoolean
判断一个值是否为布尔类型
const isBoolean = thing => thing === true || thing === false;
isPlainObject
判断值是否为一个纯粹的对象。
一个纯粹的对象指的是没有继承其他对象的属性和方法,只有自身的属性和方法的对象。在 JavaScript 中,纯粹的对象通常是通过对象字面量或者 Object.create(null) 创建的,它们没有原型链,也就是没有继承 Object.prototype 上的属性和方法。纯粹的对象在一些场景下非常有用,比如作为一个纯粹的数据容器,可以避免一些可能的命名冲突和属性覆盖问题。
const isPlainObject = (val) => {
if (kindOf(val) !== 'object') {
return false;
}
const prototype = getPrototypeOf(val);
return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in val) && !(Symbol.iterator in val);
}
- 首先通过 kindOf 函数判断 val 的类型是否为 object,如果不是则返回 false。
- 如果 val 是一个对象,则获取它的原型对象 prototype。
- 判断 prototype 是否为 null,或者是否等于 Object.prototype,或者它的原型对象是否为 null,如果都满足,则说明 val 是一个纯粹的对象。
- 最后判断 val 中是否有 Symbol.toStringTag 和 Symbol.iterator 属性,如果有则说明 val 不是一个纯粹的对象,返回 false。
isFile
判断是否为文件类型
const isFile = kindOfTest('File');
isBlob
判断是否为 Blob 对象
Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
const isBlob = kindOfTest('Blob');
isFileList
判断是否为 FileList 对象
一个 FileList 对象通常来自于一个 HTML 元素的 files 属性,你可以通过这个对象访问到用户所选择的文件。
const isFileList = kindOfTest('FileList');
isStream
判断值是否是流
// 这里先判断其是否为对象类型,再判断值得 pipe 属性是否为一个方法
const isStream = (val) => isObject(val) && isFunction(val.pipe);
isFormData
判断值是否是 FormData
FormData 接口提供了一种表示表单数据的键值对 key/value 的构造方式,并且可以轻松的将数据通过XMLHttpRequest.send() 方法发送出去。
const isFormData = (thing) => {
let kind;
return thing && (
(typeof FormData === 'function' && thing instanceof FormData) || (
isFunction(thing.append) && (
(kind = kindOf(thing)) === 'formdata' ||
// detect form-data instance
(kind === 'object' && isFunction(thing.toString) && thing.toString() === '[object FormData]')
)
)
)
}
isURLSearchParams
判断值是否是 URLSearchParams
URLSearchParams 接口定义了一些实用的方法来处理 URL 的查询字符串。它返回一个 URLSearchParams 对象,并提供了 get、set、has、append、delete等多个方法操作 URLSearchParams 对象 (developer.mozilla.org/zh-CN/docs/…)
const isURLSearchParams = kindOfTest('URLSearchParams');
trim
除去字符串开头和末尾的空格或其他不可见字符
const trim = (str) => str.trim ?
str.trim() : str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
如果参数原型上不包含 trim 方法,则通过使用 str.replace() 方法,将匹配到的字符串替换成空字符串,即可去除两端的空格或不可见字符
- ^[\s\uFEFF\xA0]+:匹配字符串开头的一个或多个空格或不可见字符。
- |: 或运算符,匹配字符串中间的空格或不可见字符。
- [\s\uFEFF\xA0]+$:匹配字符串结尾的一个或多个空格或不可见字符。
- /g:全局匹配模式,表示替换所有匹配的字符串。
forEach
遍历一个数组或一个对象,为每一项调用一个函数,其中 allOwnKeys 指的是是否对对象原型链上的值也同样调用该函数。
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);
}
}
}
findKey
查找对象是否有需要查找的键值,如果有返回键值,没有则返回 null
function findKey(obj, key) {
key = key.toLowerCase();
const keys = Object.keys(obj);
let i = keys.length;
let _key;
while (i-- > 0) {
_key = keys[i];
if (key === _key.toLowerCase()) {
return _key;
}
}
return null;
}
isContextDefined
检查上下文对象是否存在且非全局对象
const _global = (() => {
/*eslint no-undef:0*/
if (typeof globalThis !== "undefined") return globalThis;
return typeof self !== "undefined" ? self : (typeof window !== 'undefined' ? window : global)
})();
const isContextDefined = (context) => !isUndefined(context) && context !== _global;
_global 常量的赋值。这段代码使用了一个自执行函数来获取当前运行环境中全局对象。在这个自执行函数中,使用了三个全局变量分别尝试获取全局对象:
- globalThis:在现代浏览器或 Node.js 中, globalThis 表示全局对象;
- self:在 Web Worker 中, self 表示全局对象;
- window:在浏览器中, window 表示全局对象;
- global:在 Node.js 中, global 表示全局对象。
merge
合并多个对象,当有相同的键值时,靠后的对象的值更优先(即取后面覆盖前面)。
function merge(/* obj1, obj2, obj3, ... */) {
const {caseless} = isContextDefined(this) && this || {};
const result = {};
const assignValue = (val, key) => {
const targetKey = caseless && findKey(result, key) || key;
if (isPlainObject(result[targetKey]) && isPlainObject(val)) {
result[targetKey] = merge(result[targetKey], val);
} else if (isPlainObject(val)) {
result[targetKey] = merge({}, val);
} else if (isArray(val)) {
result[targetKey] = val.slice();
} else {
result[targetKey] = val;
}
}
for (let i = 0, l = arguments.length; i < l; i++) {
arguments[i] && forEach(arguments[i], assignValue);
}
return result;
}
extend
通过可变地添加对象b的属性来扩展对象a。
const extend = (a, b, thisArg, {allOwnKeys}= {}) => {
forEach(b, (val, key) => {
if (thisArg && isFunction(val)) {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
}, {allOwnKeys});
return a;
}
stripBOM
删除字节顺序标记,捕获了EF BB BF(UTF-8 BOM)
BOM 是 Unicode 字符编码的标记,但在某些情况下,它会导致问题。例如,在读取某些类型的文件时,BOM 可能会干扰文件解析,并引起错误。
const stripBOM = (content) => {
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
return content;
}
inherits
将原型方法从一个构造函数继承到另一个构造函数
const inherits = (constructor, superConstructor, props, descriptors) => {
constructor.prototype = Object.create(superConstructor.prototype, descriptors);
constructor.prototype.constructor = constructor;
Object.defineProperty(constructor, 'super', {
value: superConstructor.prototype
});
props && Object.assign(constructor.prototype, props);
}
toFlatObject
深度递归地将所有子对象元素拼接到新的对象中。
const toFlatObject = (sourceObj, destObj, filter, propFilter) => {
let props;
let i;
let prop;
const merged = {};
destObj = destObj || {};
// eslint-disable-next-line no-eq-null,eqeqeq
if (sourceObj == null) return destObj;
do {
props = Object.getOwnPropertyNames(sourceObj);
// 回一个数组,其包含给定对象中所有自有属性(包括不可枚举属性,但不包括使用 symbol 值作为名称的属性)
i = props.length;
while (i-- > 0) {
prop = props[i];
if ((!propFilter || propFilter(prop, sourceObj, destObj)) && !merged[prop]) {
destObj[prop] = sourceObj[prop];
merged[prop] = true;
}
}
sourceObj = filter !== false && getPrototypeOf(sourceObj);
} while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype);
// 当 souceObj 有值、有filter时优先执行过滤器,且返回值为true、 sourceObj 不是 Object 的原型对象时执行循环
return destObj;
}
endsWith
判断一个字符串是否由一个确定的子字符串结尾
const endsWith = (str, searchString, position) => {
str = String(str);
if (position === undefined || position > str.length) {
position = str.length;
}
position -= searchString.length;
const lastIndex = str.indexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
}
toArray
将传入值处理返回一个新数组,如果失败则返回 null
const toArray = (thing) => {
if (!thing) return null;
if (isArray(thing)) return thing;
let i = thing.length;
if (!isNumber(i)) return null;
const arr = new Array(i);
while (i-- > 0) {
arr[i] = thing[i];
}
return arr;
}
isTypedArray
检查 Uint8Array 是否存在,如果存在,则返回一个函数,该函数检查传入的值是 Uint8Array 的实例
Uint8Array 8位无符号整型数组 取值 0~255
const isTypedArray = (TypedArray => {
// eslint-disable-next-line func-names
return thing => {
return TypedArray && thing instanceof TypedArray;
};
})(typeof Uint8Array !== 'undefined' && getPrototypeOf(Uint8Array));
forEachEntry
对于对象中的每对键值,调用具有键和值的函数。
const forEachEntry = (obj, fn) => {
// 判断对象是否具有知名符号属性Symbol.iterator
const generator = obj && obj[Symbol.iterator];
const iterator = generator.call(obj);
let result;
while ((result = iterator.next()) && !result.done) {
const pair = result.value;
fn.call(obj, pair[0], pair[1]);
}
}
ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。
原生具备 Iterator 接口的数据结构如下:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
matchAll
传入一个正则表达式和一个字符串,并返回所有匹配项的数组
const matchAll = (regExp, str) => {
let matches;
const arr = [];
while ((matches = regExp.exec(str)) !== null) {
arr.push(matches);
}
return arr;
}
isHTMLForm
const isHTMLForm = kindOfTest('HTMLFormElement');
toCamelCase
将字符串转为驼峰形式(匹配 - _ \s空白)
const toCamelCase = str => {
return str.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,
function replacer(m, p1, p2) {
return p1.toUpperCase() + p2;
}
);
};
hasOwnProperty
检查一个对象是否具有指定的属性
// 它从 Object.prototype 对象中提取 hasOwnProperty 方法,并将其赋值给一个变量 hasOwnProperty
const hasOwnProperty = (({hasOwnProperty}) => (obj, prop) => hasOwnProperty.call(obj, prop))(Object.prototype);
isRegExp
判断一个值是否为正则表达式
const isRegExp = kindOfTest('RegExp');
reduceDescriptors
该函数的作用是对给定对象 obj 的属性描述符进行处理,并根据 reducer 函数的返回值来筛选出需要保留的属性描述符,最后使用 Object.defineProperties 方法重新定义对象的属性。
const reduceDescriptors = (obj, reducer) => {
// Object.getOwnPropertyDescriptor() 静态方法返回一个对象,该对象描述给定对象上而不在对象的原型链中的属性的配置
const descriptors = Object.getOwnPropertyDescriptors(obj);
const reducedDescriptors = {};
forEach(descriptors, (descriptor, name) => {
if (reducer(descriptor, name, obj) !== false) {
reducedDescriptors[name] = descriptor;
}
});
Object.defineProperties(obj, reducedDescriptors);
}
freezeMethods
传入一个对象 obj ,使其所有方法变为只读。
const freezeMethods = (obj) => {
reduceDescriptors(obj, (descriptor, name) => {
// skip restricted props in strict mode
if (isFunction(obj) && ['arguments', 'caller', 'callee'].indexOf(name) !== -1) {
return false;
}
const value = obj[name];
if (!isFunction(value)) return;
descriptor.enumerable = false;
if ('writable' in descriptor) {
descriptor.writable = false;
return;
}
if (!descriptor.set) {
descriptor.set = () => {
throw Error('Can not rewrite read-only method \'' + name + '\'');
};
}
});
}
toObjectSet
讲一个数组或者有分隔符的字符串转为一个对象
const toObjectSet = (arrayOrString, delimiter) => {
const obj = {};
const define = (arr) => {
arr.forEach(value => {
obj[value] = true;
});
}
isArray(arrayOrString) ? define(arrayOrString) : define(String(arrayOrString).split(delimiter));
return obj;
}
toFiniteNumber
将一个数值转为一个有限数值,如果传入的数值是有限数,则返回本身,若不是,则返回默认值。
const toFiniteNumber = (value, defaultValue) => {
value = +value;
return Number.isFinite(value) ? value : defaultValue;
}
generateString
随机生成一个字符串,默认长度为16,内容为含有大小写字母及数字的字符串
const ALPHA = 'abcdefghijklmnopqrstuvwxyz'
const DIGIT = '0123456789';
const ALPHABET = {
DIGIT,
ALPHA,
ALPHA_DIGIT: ALPHA + ALPHA.toUpperCase() + DIGIT
}
const generateString = (size = 16, alphabet = ALPHABET.ALPHA_DIGIT) => {
let str = '';
const {length} = alphabet;
while (size--) {
str += alphabet[Math.random() * length|0]
}
return str;
}
isSpecCompliantForm
判断传入值是否是FormData对象,如果是返回true,否则返回false。
function isSpecCompliantForm(thing) {
return !!(thing && isFunction(thing.append) && thing[Symbol.toStringTag] === 'FormData' && thing[Symbol.iterator]);
}
toJSONObject
将一个对象转换为 JSON 对象,其中包含了对象的所有可枚举属性和值。
const toJSONObject = (obj) => {
// 用于存储访问过的对象
const stack = new Array(10);
const visit = (source, i) => {
if (isObject(source)) {
if (stack.indexOf(source) >= 0) {
return;
}
if(!('toJSON' in source)) {
stack[i] = source;
// 如果是数组或对象,再递归遍历将里面的属性转为 JSON 对象
const target = isArray(source) ? [] : {};
forEach(source, (value, key) => {
const reducedValue = visit(value, i + 1);
!isUndefined(reducedValue) && (target[key] = reducedValue);
});
stack[i] = undefined;
return target;
}
}
return source;
}
return visit(obj, 0);
}
isAsyncFn
判断传入值是否为一个 Async 函数
const isAsyncFn = kindOfTest('AsyncFunction');
isThenable
判断一个函数是否能使用 then 方法。
const isThenable = (thing) =>
thing && (isObject(thing) || isFunction(thing)) && isFunction(thing.then) && isFunction(thing.catch);
总结
OK~ 读完了。其实读工具函数,我觉得更多的是学习更优秀的编码风格(还能夯实基础~),思考工具函数的用意,灵活将这些思维或技巧运用到自己开发的项目中去,同时也能提升自己的编码水平。