本文参加了每周一起学习200行源码共读活动
Axios
的工具函数位于lib/utils
。 点此前往
isArray
/**
* Determine if a value is an Array
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Array, otherwise false
*/
function isArray(val) {
return Array.isArray(val);
}
isArray
函数主要是判断是否是数组。
判断数组一般有以下几种方法:
type of
这是最笨,也是很不优雅的方法。首先使用 type of
判断是否是 Object
,之后再判断有没有 length
这个属性。
function isArray(val) {
return typeof val === "object" && val.hasOwnProperty("length");
}
这个方法最大的缺陷是,不管定义任何类或者 object
,它的对象都是 object
,如果定义的类或者 object
中带有 length
属性。则使用这个检测方法也会返回 true
。
instanceof
Instanceof
是一种运算符,主要用来判断 A
是否是 B
的实例。其基本原理是当 A
的 __proto__
指向 B
的 prototype
时,就认为 A
就是 B
的实例。
如下所示:任意两个 array
的 __proto__
是相等的,并且任意一个 array
的 __proto__
与 Array.prototype
也是相等的。
let a = [];
let b = [1,2,3];
console.log(a.__proto__ === b.__proto__); // true
console.log(a.__proto__ === Array.prototype); // true
console.log(a instanceof Array); // true
所以可以实现如下代码:
function isArray(val) {
return val instanceof Array;
}
instanceof 的主要问题是,无法判断使用 ifram 生成的。
constructor
所有实例都会从类上继承一个 constructor 属性。使用 constructor 与 instanceof 有异曲同工之妙,当然问题也是一样的。
代码如下:
function isArray(val) {
return val.constructor === Array;
}
Object.prototype.toString.call()
这个方法可以说是一个比较好的方法,能够准确的判断出是什么类型。但是它的性能相对来说要差很多。
function isArray(val) {
return Object.prototype.toString.call(val) === "[object Array]";
}
Array.isArray
这是 Array
的内置方法,与Object.prototype.toString.call()
差不多,但是性能要比 Object.prototype.toString.call()
好,而这个也是 axios
官方在使用的方法。代码如下:
function isArray(val) {
return Array.isArray(val);
}
isUndefined
/**
* Determine if a value is undefined
*
* @param {Object} val The value to test
* @returns {boolean} True if the value is undefined, otherwise false
*/
function isUndefined(val) {
return typeof val === 'undefined';
}
isUndefined
函数主要是判断是否是 undefined
。
这个十分简单,不过在判断 undefined
方面也有多种方法。
取反
直接使用取反可以判断 undefined
,但是需要注意的是,取反是一个隐式转换,即先转为 boolean
判断。所以如果使用取反,则 undefined null false 0 ""
都会生效 需要对 undefined
进行单独区分的时候,这个就没有作用了。
===
可以使用强等于来判断是否时 undefined
,注意一定是强 ===
。因为如果使用弱等于 ==
,则无法分辨出 null
和 undefind
。
在这里需要提一嘴 null
和 undefined
的区别,在 js
的最初版本中,只有 null
,但是后来由于种种原因,又设置了 undefined
,并标明:
undefind
用来代表变量的最初始值,即应该有这个变量或属性,但是并未定义。这一点可以从声明一个变量并打印的结果 (undefined
) 、直接使用一个未声明的变量的异常 (xxx is not defined
) 、函数直接return;
或者不写return
的返回值 (undefined
) ,调用对象不存在的属性的结果 (undefined
) 的来看出。null
用来代表一个空对象,或者可以说是一个空指针,以此来代表没有相应的对象。通俗点说就是,栈中的变量没有指向堆中的内存对象。所以一般使用xx = null
来释放内存对象。这个从typeof null
的结果 (object
) 上就可以看出来,显然 js 认为 null 是一个对象,只不过是引用为空的对象。
以此来看,其实 null
和 undefind
相差不大,有也只是一些细微的差别(主要是语义上)。
typeof
这个是官方的使用方法,这个是比较省事也比较准确的一种方法。
Object.prototype.toString.call()
这是直接调用了原型,输出对象的类型 '[object Undefined]'
。这个方法主要是用来判断各种的内置类型,并且是最准的,所以不仅仅是 Undefined
,包括上面的 Array
、Object
、浏览器的 Window
都可以。
isBuffer
/**
* Determine if a value is a Buffer
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Buffer, otherwise false
*/
function isBuffer(val) {
return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
&& typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val);
}
这个是判断是否是 buffer
对象。
因为 js
自身是不能很好的处理二进制数据类型的,这在平常使用中没有太大问题,但是一旦碰到流式处理的场景,就显得捉襟见肘。因此在 Node.js
中,定义了一个Buffer
类,该类用来创建一个专门存放二进制数据的缓存区。
但是 buffer
既不是单纯的流,也不是单纯的二进制,而是一个缓冲区使用流的形式处理二进制数据。不准确的说,可以将 buffer
对象理解为 go
中的通道或者是消息队列,也可以看做是一种滑动窗口。
buffer
对象占用的内存空间是不计算在 node
进程内存空间限制上的,即它的内存,分配在 V8
堆栈外。但是 buffer
空间也有上限,并且这个上限是固定的,如果向 buffer
中存入的数据到达上限则不能再存入,需要先取后存。这种其实就是一种流的形式,所以 buffer
其实就是一个缓冲区。
官方的方法主要是通过判断:非空数据 + 对象构造函数的 isBuffer
是否是 function
。
当然这个方法并不是很准确,比如我使用如下的方法伪造一个 buffer
就能通过检测。
let fakeBuffer = {
constructor: {
isBuffer: function(val) {
return true;
}
}
}
isBuffer(fakeBuffer) // true
在 node
环境中可以使用 toString.call(val) === [object Uint8Array]
,但是在浏览器环境下并不支持 buffer
。
isArrayBuffer
/**
* Determine if a value is an ArrayBuffer
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an ArrayBuffer, otherwise false
*/
function isArrayBuffer(val) {
return toString.call(val) === '[object ArrayBuffer]';
}
检查是否是 ArrayBuffer
对象。
ArrayBuffer
对象是一个通用的、固定长度的原始二进制数据缓冲区,数据结构为字节数组,它不能直接读写,只能通过视图进行操作。
它可以通过两种视图进行操作:TypedArray
(简单类型的二进制数据读写) 以及 DataView
(复杂类型的二进制数据读写) 。而 buffer
其实是 TypedArray
的一种实现。
所以,ArrayBuffer
是一个基础、原始的缓冲区实现,所以 ArrayBuffer
也是一个内置对象。
isFormData
/**
* Determine if a value is a FormData
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an FormData, otherwise false
*/
function isFormData(val) {
return toString.call(val) === '[object FormData]';
}
检查是否是 FormData
对象,FormData
对象可以使用 append
k/v 的方式生成一个表单数据。
toString.call()
是 Object.prototype.toString.call()
的简写形式。就像是浏览器中 Websocket
是 windows.Websocket
的简写形式一样。
当然,这里官方其实做了下变量声明,在 utils.js
的头部声明了一个 var toString = Object.prototype.toString;
,但其实直接使用 toString.call()
也是可以的。
因为 FormData
属于内置对象,所以可以直接使用 toString.call()
的方法进行检测。
isArrayBufferView
/**
* Determine if a value is a view on an ArrayBuffer
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false
*/
function isArrayBufferView(val) {
var result;
if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) {
result = ArrayBuffer.isView(val);
} else {
result = (val) && (val.buffer) && (isArrayBuffer(val.buffer));
}
return result;
}
判断是否是 ArrayBuffer
的视图。
isString
/**
* Determine if a value is a String
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a String, otherwise false
*/
function isString(val) {
return typeof val === 'string';
}
判断是否是字符串,最简单的方法就是 typeof
,也可以使用 Object.prototype.toString.call()
当然在做字符串判断时还需要注意一点,是否是非空的字符串,这一块在实际使用中根据自己需求来即可。
isNumber
/**
* Determine if a value is a Number
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Number, otherwise false
*/
function isNumber(val) {
return typeof val === 'number';
}
判断是否是数字,官方的这个方法可以用来做简单的数字判断,除此之外,也有可用于其他特殊场景的一些判断方法。详见数字判断工具函数
isObject
/**
* Determine if a value is an Object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Object, otherwise false
*/
function isObject(val) {
return val !== null && typeof val === 'object';
}
判断是否是 object
。需要注意的是,不论是 array
、map
、json
、浏览器中的 window
,他们的 typeof
都是 object
。如果想判断是否是 json
,较好的方法是使用 Object.prototype.toString.call()
,但是一些,当然下面的 isPlainObject
也不失为一种好方法。
isPlainObject
/**
* Determine if a value is a plain Object
*
* @param {Object} val The value to test
* @return {boolean} True if value is a plain Object, otherwise false
*/
function isPlainObject(val) {
if (toString.call(val) !== '[object Object]') {
return false;
}
var prototype = Object.getPrototypeOf(val);
return prototype === null || prototype === Object.prototype;
}
这个是用来判断一个非官方标准定义的 object - PlainObject
。
PlainObject
指的是一个纯粹的对象,即 js 中原生的 {} 或使用 new object
创建的对象。
关于这个检测方法的详细解释可以看这篇提问的高赞回答
isDate
/**
* Determine if a value is a Date
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Date, otherwise false
*/
function isDate(val) {
return toString.call(val) === '[object Date]';
}
用来判断是否是一个 Date
对象。
isFile
/**
* Determine if a value is a File
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a File, otherwise false
*/
function isFile(val) {
return toString.call(val) === '[object File]';
}
判断是否是一个文件对象,一般是用在文件上传上。
isBlob
/**
* Determine if a value is a Blob
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Blob, otherwise false
*/
function isBlob(val) {
return toString.call(val) === '[object Blob]';
}
判断是否是一个类文件对象。
isFunction
/**
* Determine if a value is a Function
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Function, otherwise false
*/
function isFunction(val) {
return toString.call(val) === '[object Function]';
}
判断是否是函数。
isStream
/**
* Determine if a value is a Stream
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Stream, otherwise false
*/
function isStream(val) {
return isObject(val) && isFunction(val.pipe);
}
判断是否是流。主要是通过是否存在指定方法来判断的。 因为 stream
本身的原型是一个 object
。stream
自身更是一个 function
。
isURLSearchParams
/**
* Determine if a value is a URLSearchParams object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a URLSearchParams object, otherwise false
*/
function isURLSearchParams(val) {
return toString.call(val) === '[object URLSearchParams]';
}
判断是否是一个 URL
的查询字符串对象。
URLSearchParams
是一个原生 api
。使用此 api
可以快速生成一个 get url
的 params
字符串,而不需要去手动拼接。
trim
/**
* Trim excess whitespace off the beginning and end of a string
*
* @param {String} str The String to trim
* @returns {String} The String freed of excess whitespace
*/
function trim(str) {
return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
}
去除前后空格。这里进行了一下判断,如果在一些没有 str.trim
的浏览器中,使用后面的正则替换方法来删除空字符。
isStandardBrowserEnv
/**
* Determine if we're running in a standard browser environment
*
* This allows axios to run in a web worker, and react-native.
* Both environments support XMLHttpRequest, but not fully standard globals.
*
* web workers:
* typeof window -> undefined
* typeof document -> undefined
*
* react-native:
* navigator.product -> 'ReactNative'
* nativescript
* navigator.product -> 'NativeScript' or 'NS'
*/
function isStandardBrowserEnv() {
if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' ||
navigator.product === 'NativeScript' ||
navigator.product === 'NS')) {
return false;
}
return (
typeof window !== 'undefined' &&
typeof document !== 'undefined'
);
}
判断是否是标准浏览器环境,对于 isStandardBrowserEnv
,首先我们先看下 isStandardBrowserEnv
的判断方法:使用 navigator
的 product
属性 和 window
及 document
是否为空。
对此我们会有几个疑惑:
navigator
是一个不推荐的对象,原因是很有可能会被浏览器使用者修改。但是这个方法只是用来做前置处理,即使被修改了,影响的也只是一些参数。并且对于一些主流浏览器来说,修改navigator
也并非易事。window
和document
理论上也可以被修改或删除。这一块可以这么理解,一个是对于一些主流浏览器来说,修改这种浏览器原生对象并非易事,另一个是window
和document
都已经不存在了,代表网页也可能挂掉了,这个时候对于正常的用户来说,肯定是无法使用,那判断准不准确也没什么关系。
因为 axios
既可以在浏览器中发送请求,又可以在 node
环境中使用,所以肯定需要判定是在什么环境下。但是这里的 isStandardBrowserEnv
其实并不是为了判断使用什么方式发送请求的,而是为了做一些前置处理,比如设置一些 header
、cookie
,做一些同源策略判断等等。
Axios
在发送请求前做了另一种判断,可以在 /lib/defaults
下的 getDefaultAdapter
方法中看到(如下)。根据当前是否有 XMLHttpRequest
或 process
来判断是标准浏览器还是 node
环境。如果两者皆不是则直接返回一个空的,就无法进行调用了。
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
forEach
/**
* 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];
}
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
自己实现的一个增强型 forEach
。逻辑很简单:
- 如果不是对象,直接包装为数组,并按数组的方式来使用。
- 如果是数组,遍历数组中的每一个元素,并将元素、下标、数组本身按顺序传给一开始传入的
fn
处理函数。 - 如果是
json
,遍历json
中的每一对元素,并将v
、k
、json
本身按顺序传给一开始传入的fn
处理函数。
原生的 forEach
只能遍历数组,json
需要使用 Object.keys
拍扁后进行遍历。而这个增强型数组则可以直接遍历 array
和 json
,而不需要关注具体的对象到底是 json
还是 array
。
因为 axios
需要对 headers
( json
格式 )、请求 method
( array
格式 )、拦截器 ( array
格式 ) 等对象做遍历处理,这样一来就能精简很多代码,并且也能统一遍历的使用方法。
merge
/**
* Accepts varargs expecting each argument to be an object, then
* immutably merges the properties of each object and returns result.
*
* When multiple objects contain the same key the later object in
* the arguments list will take precedence.
*
* Example:
*
* ```js
* var result = merge({foo: 123}, {foo: 456});
* console.log(result.foo); // outputs 456
* ```
*
* @param {Object} obj1 Object to merge
* @returns {Object} Result of all merge properties
*/
function merge(/* obj1, obj2, obj3, ... */) {
var 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 (var i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue);
}
return result;
}
这个方法是用来合并配置信息用的,比如使用 axios
设置了多次配置,最后配置是从后向前合并的,就是用了这个方法。
这个函数设计的思路比较精妙,首先不断的分解问题(递归),分解到最低的时候对相应的 k v
或对象进行合并处理并提交至顶层的 result
中。
js
原生有一个合并方法 object.assign
。但是这个方法有一个缺陷,对于元素,只要存在都是直接覆盖掉。比如:
// 有这样两个对象,我想将他们合并起来。
let test1 = {"info":{"name":"elaissama","age":18},id: 3}
let test2 = {"info":{"name":"elais"},id: 5}
// 我的期望值是
let result = {"info":{"name":"elias","age":18},id: 5}
// 但是,如果使用了 object.assign ,结果就成了
result = {"info":{"name":"elias"},id: 5}
在这种情况下,object.assign
是无法使用的,所以就需要使用上面的方法。
extend
/**
* Extends object a by mutably adding to it the properties of object b.
*
* @param {Object} a The object to be extended
* @param {Object} b The object to copy properties from
* @param {Object} thisArg The object to bind function to
* @return {Object} The resulting value of object a
*/
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
这是一个继承方法,目的是将 B
对象的一些属性和方法拷贝到 A
对象中。在上一篇文章中,我们的图例标明了,最外层暴露给我们用的 axios
其实是使用这个方法继承了内部的 Axios
类上的方法和属性。
stripBOM
/**
* Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
*
* @param {string} content with BOM
* @return {string} content value without BOM
*/
function stripBOM(content) {
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
return content;
}
这个方法是用来删除字节序标识符。
字节序标识符主要是用来告诉计算机的字节读取顺序,常被用来当做标示以UTF-8
、UTF-16
或UTF-32
为编码的文件。
之所以要删除这个字节序标识符,是因为 UTF-8
的编码方式不同,不使用字节序标识符也可以正确读取数据,而且在一些特定情境下还会导致解析出错。