#
baseGetTagjQuery 有一个class2type
这样的一个内部属性,该属性利用Object.prototype.toString()
方法,得到的对象字符串,用该字符串解析准确的对象类型, Lodash 也利用了该方法,只不过个人认为,Lodash 中不能以用 jQuery 的眼光看待 Object.prototype.toString
jQuery 中 class2type 是一个对象,该对象将 Boolean Number String Function Array Date RegExp Object Error Symbol
等常见类型用 each 分别组合成 toString 结果填充到对象中
// jquery 中的 class2type
let class2type = {};
"Boolean Number String Function Array Date RegExp Object Error Symbol"
.split(" ")
.map(function( name, i ) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
})
console.log(class2type)
// 最终 class2type 是一个关系映射表
/*
{ '[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Object]': 'object',
'[object Error]': 'error',
'[object Symbol]': 'symbol' }
*/
const type = function( obj ) {
if ( obj == null ) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[ class2type.toString.call( obj ) ] || "object" :
typeof obj;
}
console.log(type(/d/)) //=> regexp
console.log(type({})) //=> object
console.log(type(null)) //=> null
console.log(type([1,2,3])) //=> array
// 判断是否是一个数组
console.log( type([1,2,3]) === 'array') //=> true
而 Lodash 中以函数为单位,将每一个部分独立出来和 jQuery 类似
// lodash 中的 class2type
let argsTag = '[object Arguments]',
arrayTag = '[object Array]',
asyncTag = '[object AsyncFunction]',
boolTag = '[object Boolean]',
dateTag = '[object Date]',
errorTag = '[object Error]',
funcTag = '[object Function]',
genTag = '[object GeneratorFunction]',
numberTag = '[object Number]',
objectTag = '[object Object]',
proxyTag = '[object Proxy]',
regexpTag = '[object RegExp]',
stringTag = '[object String]';
let _nativeObjectToString = Object.prototype.toString;
function _objectToString(value) {
return _nativeObjectToString.call(value);
}
function _baseGetTag(value) {
return _objectToString(value);
}
function type (value) {
return _baseGetTag(value)
}
function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
function isRegExp(value) {
return _baseGetTag(value) == regexpTag;
}
function isFunction(value) {
// ...
var tag = _baseGetTag(value);
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}
console.log(type(/d/)) //=> [object RegExp]
console.log(type({})) //=> [object Object]
console.log(type(null)) //=> [object Null]
console.log(type([1,2,3])) //=> [object Array]
console.log( Array.isArray([2,3,4]))//=> true
console.log( isFunction(function* (){}))//=> true
console.log( isFunction(function (){}))//=> true
代码很长,但思维缜密,每个部分,每一个方法,属性可以说都是独立于整个类库中baseGetTag 也是 Lodash.getTag 的基函数
#
baseValuesLodash.values
是由该方法生成的,Lodash.values 方法返回一个指定对象可枚举
属性值的数组
console.log( Lodash.values([1,2,3,4,5,6]) ) //=> [ 1, 2, 3, 4, 5, 6 ]
console.log( Lodash.values('hi') ) //=> [ 'h', 'i' ]
console.log( Lodash.values(100) ) //=> []
baseValues 的具体步骤,操作一得到指定对象的可枚举的属性,Object.keys 可直接获取,操作二 baseEach 遍历出可枚举属性的属性值,只是这个过程中加入了一个baseMap
操作, baseMap 是实现 map 的基函数,但内部还是用 createBaseEach 完成,回调分别是键,值和当前遍历的对象
baseMap 基本是由 baseEach 遍历的结果
#
createAssigner 基函数将 sources 中的对象的属性依次赋值给 target 对象, 同属性时会被覆盖,这里直接借 MDN 其特点
这里的主角是 Lodash 不是 ES6 下面就来看看 Lodash 中的 assign
createAssigner 接收一个回调assigner
,该回调主要用来为返回的函数形参 object 添加 rest 参数 sourcces 中对象的属性,也是生成baseRest
这个私有方法的基函数,lodash 中可接收若干实参的函数基本离不开 baseRest 方法, assign 操作也会离不开 rest 参数所以 createAssigner 最后的返回是被 baseRest 包裹,实际上就是返回了一个 baseRest 方法,可对若干的源对象进行遍历操作
// ...
// 为 assigner 回调形参为 3 个以上时提供自定义函数 customizer
customizer = (assigner.length > 3 && typeof customizer == 'function')
? (length--, customizer)
: undefined;
// 包装成对象
object = Object(object);
while (++index < length) {
// ...
// 注意回调可接收四个参数
assigner(object, source, index, customizer);
}
// ...
源码的 createAssigner 方法中最值得注意的地方有两处
Lodash.assignWith
中有解释,意思就是如果是以 assignWith 这样的方式进行属性复制则可以自定义复制值的最终结果,如果为函数时可以接收五个参数objValue, srcValue, key, object, source
,可以看作是 createAAssigner 提供的一个钩子函数#
copyObject 私有方法上述的 createAssigner 回调 assigner, 在 Lodash.assign() 或 Lodash.assignIn() 方法指定的是 copyObject() 为 assigner, copyObject 的参数二也是直接用 Object.keys 或 for…in 遍历出来的所有属性名数组
/**
* Copies properties of `source` to `object`.
*
* @private
* @param {Object} source The object to copy properties from.
* @param {Array} props The property identifiers to copy.
* @param {Object} [object={}] The object to copy properties to.
* @param {Function} [customizer] The function to customize copied values.
* @returns {Object} Returns `object`.
*/
function copyObject(source, props, object, customizer) {}
该方法只能浅复制
该方法就注意以下几点:
undefined
,null
,-0
,0或+0
,NaN
,''
(空字符串)时会将属性当作一个关键字#baseAssignValue()
与#assignValue()
方法的区别就是, baseAssignValue 是直接用中括号操作符进行赋值,而 assignValue 会进行一次SameValueZero
的判断,如果不相等则会利用 baseAssignValue 赋值成对象的一个属性,下面则是_createAssigner
实现过程
function _eq(value, other) {
return value === other || (value !== value && other !== other);
}
function _baseAssignValue(object, key, value) {
object[key] = value;
}
function _assignValue(object, key, value) {
var objValue = object[key];
if (!(Object.prototype.hasOwnProperty.call(object, key) && _eq(objValue, value)) ||
(value === undefined && !(key in object))) {
_baseAssignValue(object, key, value);
}
}
function _copyObject(source, props, object, customizer) {
var isNew = !object;
object || (object = {});
// `undefined`,`null`,`-0`,`0或+0`,`NaN`,`''` 其余都会为 true
var index = -1,
length = props.length;
while (++index < length) {
var key = props[index];
// 自定义的结果值
var newValue = customizer
// 分别为五个参数
// objValue, srcValue, key, object, source
? customizer(object[key], source[key], key, object, source)
: undefined;
if (newValue === undefined) {
newValue = source[key];
}
if (isNew) {
_baseAssignValue(object, key, newValue);
} else {
_assignValue(object, key, newValue);
}
}
return object;
}
let a = {'name': 'qlover', 'age': 20}
let b =_copyObject(a, Object.keys(a), { 'c': 100 },
function(objValue, srcValue, key, object, source){
return 'preix_' + srcValue
})
console.log(a)//=> { name: 'qlover', age: 20 }
console.log(b)//=> { c: 100, name: 'preix_qlover', age: 'preix_20' }
assign,assignIn 和 assignWith 三者在 lodash 中的区别如下
作一个补充:
function baseRest(func, start) {
return setToString(overRest(func, start, identity), func + '');
}
核心版本中#setToString()
方法其实就是#identity()
方法,这是因为在完整版本中 setToString 可重写对象的 toString 方法
该方法松散的基于Object.assign()
,作用很简单就是对象的复制,将所有可枚举属性的值从一个或多个源对象复制到目标对象,返回目标对象
assign 与 assignIn 不同,只会复制可枚举属性,也就是说像原型上不可枚举的属性是不会被复制的
但是凡是没有绝对,就比如当利用JSON.parse()
方法解析一个包含有不可枚举的自身属性时,就是个例外,这里就不得不提一个叫原型污染
的东西,原型污染就是当一个不可以枚举的属性被重新复制到了目标对象上引起的程序错乱,是前端的一种可利用攻击方式
let o1 = { a: 1}
let o2 = {}
console.log(o1.a)//=> 1
console.log(o2.a)//=> undefined
Object.assign(o2, { b: 2, '__proto__': { a : 10}})
console.log(o1.a)//=> 1
console.log(o2.a)//=> undefined
利用Object.assign()
方法对对象 o2 作一个扩展功能,如果Object.assign()
方法将源对象的__proto__
属性成功的重写,那么就会影响到所有继承 o2 原型的对象,但在这里还是很安全的并没有发生所期望的事情,下面则就不同了
let o1 = { a: 1}
let o2 = {}
console.log(o1.a)//=>1
console.log(o2.a)//=> undefined
Object.assign(o2, JSON.parse('{ "b": 2, "__proto__": { "a" : 10}}'))
console.log(o1.a)//=> 1
console.log(o2.a)//=> 10
同样的是利用Object.assign方法,不同的是源对象是一个被 json 解析后的对象, json 解析后的对象有个特点,就是该对象是一个"纯"的对象,非常纯,纯到解析出来的属性全部都是可以枚举的属性
let o = { b: 1}
console.log( Object.keys(JSON.parse('{ "b": 2, "__proto__": { "a" : 10}}')) )
//=> [ 'b', '__proto__' ]
console.log( Object.keys(o))
//=> [ 'b' ]
也就是说如果解析的属性名其原型或本身都会存在时,会丢失属性的修饰,从这里也就不难看出,不管是用 Object.assign 或是用基于 Object.assign 的 Lodash.assign 都会存在这样的一个问题
lodash 的解决方案就是利用Object.prototype.hasOwnProperty()
和in
运算符,hasOwnProperty
和in
都检测一个对象是否含有特定属性,但只有in
不会忽略原型链上的属性,回到上述的_baseAssignValue
和_assignValue
,lodash核心版本中 Lodash.assign 是私有方法并没有暴露出来,是因为核心版本中的处理并非到达_assignValue
方法内部,而完整版中_assignValue
方法排除了这个可能,这是就是因为 assign 在核心版本中除了 null undefined 这些参数其它都会直接 baseAssignValue 直接赋值
#
baseRest当一个函数可以接收若干个参数,ES5 这之前都提供了一个叫 arguments 的类数组对象,不管是什么实参,一旦传入都会被 arguments 接收到,但 arguments 很特殊不能像通常数组一样使用,ES6 这之后加了一个新特性叫 rest 参数,可以做到像 python *args
这样的形参形式,可以接收若干实参的数组,而Lodash.rest
可以理解为 arguments 到 rest 参数的过渡
为了实现像 ES6 这样的 rest 参数 lodash 是这样做的。
function _overRest(func, start, transform) {
// 取函数形参的第1个参数开始,将第一个参数后面的所有参数当作 rest 参数
start = Math.max(start === undefined ? (func.length - 1) : start, 0);
return function _overRest_return() { // 最终返回的函数,也是最终接收参数的地方
// 重置 rest 参数
var args = arguments,
index = -1,
length = Math.max(args.length - start, 0),
array = Array(length); // 转换 arguments 的长度数组
// arguments 转换成真数组
while (++index < length) {
array[index] = args[start + index]; // 转置 arguments 的元素到新数组中
}
index = -1;
// 处理 args
// 默认 start 为 1 也就是数组第二个元素
// 默认就将 start 后所有的元素当作一个新的数组
// start 后面的数组当作是回调的真 rest 参数
// 而 start 前面的可以看作是回调 rest 参数的固定形参
var otherArgs = Array(start + 1);
while (++index < start) {
otherArgs[index] = args[index];
}
otherArgs[start] = transform(array);
console.log('s>', start, otherArgs)
// args 最后作为参数返回给回调
// 最后的 args 也是一个真数组
return func.apply(this, otherArgs);
};
}
function _identity(value){return value;}
function _baseRest(func, start){
return _identity(_overRest(func, start, _identity));
}
function _rest(func, start){
return _baseRest(func, start);
}
关键在于 start 参数, start 参数标识了传入实参与 rest 参数间的分隔,也就是将形参部分分成了已知和未知,未知就当作了 rest 参数,函数中应用了这一点,最后将参数传回回调,rest 就完成了
let revice = _rest(function(name, car){
console.log(name, car)
})
revice('qlover', 'BMW', 'Audi', 'Alpha')
//=> qlover [ 'BMW', 'Audi', 'Alpha' ]
revice = function(name, ...car){
console.log(name, car)
}
revice('qlover', 'BMW', 'Audi', 'Alpha')
//=> qlover [ 'BMW', 'Audi', 'Alpha' ]
如何做到回调最后一个形参始终为 rest 参数从两点入手:
_overRest_return
的实参个数当 start 为func.length - 1
时,即表示为 func 最后一个形参,_overRest_return
接收的实参(args)个数是多于 start 的,那么 args 的 start 后面的部分(args.length - start
)则为 rest 参数
当 start 为指定第几个形参为最后一个形参时,也同样如此,指定 start 时,一定要分好 func 的形参避免传递不了参数,附上一个实例:
lodash.rest(function(name, bmw, cars){
console.log('cars is<', cars)
}, 2)('qlover', 'BMW', 'Audo', 'Alpha')
// cars is< [ 'Audo', 'Alpha' ]
lodash.rest(function(name, bmw, cars){
console.log('cars is<', cars)
})('qlover', 'BMW', 'Audo', 'Alpha')
// rest is< [ 'Audo', 'Alpha' ]
完整版的 Lodash.spread 可以做 rest 逆运算 ES6 已经实现了该操作,并且还为该操作另添加了一个新语法,...
运算符,该运算符不仅可以做剩余操作也可以做扩展操作,详情可见
#
baseMap返回一个遍历后的新数组,内部使用 baseEach 对参数 collection 进行遍历,同样的会将 value,key,object 三个参数作为形参传递给迭代回调,并且内部维护的index
作为索引为新数组依次赋值为迭代回调的返回值
function _baseMap(collection, iteratee) {
var index = -1,
result = isArrayLike(collection) ? Array(collection.length) : [];
baseEach(collection, function(value, key, collection) {
result[++index] = iteratee(value, key, collection);
});
return result;
}
Array.prototype.sort([compareFunction(firstEl, secondEl)])
方法用原地算法(内排)对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的,如果compareFunction
省略,元素按照转换为的字符串的各个字符的Unicode位点进行排序
原地算法不依赖额外的资源或者依赖少数的额外资源,仅依靠输出来覆盖输入的一种算法操作,节省内存
以形参a和b为例:
且 a和b的值都应该是同输入同结果,有点像纯函数,输入什么就会返回什么
下面以 Array.prototyp.sort
方法例子:
排列数组:
console.log([9,3,-5,1,-2].sort())//=> [ -2, -5, 1, 3, 9 ]
console.log([9,3,-5,1,-2].sort( (x,y) => x - y ))//=> [ -5, -2, 1, 3, 9 ]
console.log([9,3,-5,1,-2].sort( (x,y) => x > y ? -1 : 1 ))//=> [ 9, 3, 1, -2, -5 ]
排列对象,排列对象的修改虽然不能直接用数组规则但可以用键的值比较:
var users = [
{ 'car': 'bmw', 'money': 100 },
{ 'car': 'audi', 'money': 300 },
{ 'car': 'Alpha', 'money': 200 },
]
// 以 money 值升序排列
// console.log( users.sort( (x,y) => x.money - y.money ) )
/*//=>
[ { car: 'bmw', money: 100 },
{ car: 'Alpha', money: 200 },
{ car: 'audi', money: 300 } ]
*/
// 以 money 值升序降序
console.log( users.sort( (x,y) => x.money > y.money ? -1 : 1 ) )
/*//=>
[ { car: 'audi', money: 300 },
{ car: 'Alpha', money: 200 },
{ car: 'bmw', money: 100 } ]
*/
详见
该方法属于内排
而 Loadash 核心版本中也以Array.prototype.srot()
为中心进行排序#compareAscending()
是 loadash 默认的排序 compareFunction,默认以升序排列
function _sortBy(collection, iteratee) {
var index = 0;
iteratee = _baseIteratee(iteratee);
return _baseMap(_baseMap(collection, function(value, key, collection) {
return { 'value': value, 'index': index++, 'criteria': iteratee(value, key, collection) };
}).sort(function(object, other) {
return _compareAscending(object.criteria, other.criteria) || (object.index - other.index);
}), _baseProperty('value'));
}
sortBy 方法在对 collection 的类型排序时,会将 collection 遍历成一个对象组成的数组,每个对象赋与 index 唯一索引值,利用该索引值排序