【若川视野 x 源码共读】 第19期 | axios 工具函数

131 阅读14分钟

本文参加了由 公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是源码共读的第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~ 读完了。其实读工具函数,我觉得更多的是学习更优秀的编码风格(还能夯实基础~),思考工具函数的用意,灵活将这些思维或技巧运用到自己开发的项目中去,同时也能提升自己的编码水平。