#
arrayPush从字面意思上看可以看出是一个数组 push 操作,于数组原型上的 push 操作不同的是,数组原型上的 push 只能一次 push 一个元素,如果该元素又是一个数组,则结果也会将这个数组当作一个元素作为 push 的值
而 arrayPush 利用Function.prototype.apply
方法特性,可以类似Array.prototype.concat
方法一样,将 push 的多个元素一依次追加到目标末尾
#
baseEach 私有方法 c1是创建baseEach
或baseEachRight
这两个私有方法的源方法,接收两个参数第一个是迭代回调,第二个是布尔值,可指定从右到左迭代
function _createBaseFor(fromRight) {
return function(object, iteratee, keysFunc) {
// ...
return object;
};
}
function _baseForOwn(object, iteratee) {
// 此处的 false 为 _createBaseFor 传递参数表示是否从右至左遍历
return object && _createBaseFor(false)(object, iteratee, Object.keys);
}
function _createBaseEach(eachFunc, fromRight) {
return function(collection, iteratee) {
// ...
return collection;
};
}
var baseEach = _createBaseEach(_baseForOwn);
var baseEachRight = _createBaseEach(_baseForOwn, true);
#
createBaseForLodash.forIn 和 Lodash.forOwn 的基函数,什么是基函数,可以理解为 Lodash.forOwn 函数是由该函数生成,或者是 Lodash.forIn 函数的母亲是该函数
这里的两个生成函数并不是核心里面的,但核心中有一个叫 baseFor
的重要函数, baseForOwn
函数也是由 baseFor 该生成专门用来可遍历类数组
的函数
function _createBaseFor(fromRight) {
return function(object, iteratee, keysFunc) {
var index = -1,
iterable = Object(object),
props = keysFunc(object),
length = props.length;
while (length--) {
var key = props[fromRight ? length : ++index];
if (iteratee(iterable[key], key, iterable) === false) {
break;
}
}
return object;
};
}
该方法很简单,就是做一个类数组遍历操作,而这里的对象遍历操作避免了 for…in 操作,可能有的人注意到了。
_createBaseFor
方法可以接收一个参数,表示其是否从右至左遍历,虽然不知道为什么这样,是为了方便也好还是怎样,收集资料后个人觉得可能是因为 for…in 遍历出来的顺序不一定按顺序。
#
createBaseEachfunction _createBaseEach(eachFunc, fromRight) {
return function _createBaseEach_return(collection, iteratee) {
if (collection == null) {
return collection;
}
// 类数组处理
if ( !Lodash.isArrayLike(collection)) {
return eachFunc(collection, iteratee);
}
// 通常集合处理
var length = collection.length,
index = fromRight ? length : -1,
iterable = Object(collection);
while ((fromRight ? index-- : ++index < length)) {
if (iteratee(iterable[index], index, iterable) === false) {
break;
}
}
return collection;
};
}
jQuery 的链式操作应该是10多年前的一个新潮思想, jQuery 的链式操作做到了do less, write more
其原理就是在每个方法后面返回了 this, 这里看得第一眼就让我想起了链式操作,当集合collection
是数组走eachFunc
也好还是对象走iteratee
最终结果不仅遍历了元素且返回了原集合,该方法内部对类数组
和通常可遍历的对象都作了处理,如果是类数组则用上面的 _createBaseFor
生成的函数作遍历,其余的转换成对象,对对象的属性值作遍历操作最后返回遍历集合, 这整个函数调来调去就是为了生成baseEach
方法。
createBaseFor
是生成遍历函数的基函数,也可以理解为抽象类,作用就是可以让一个函数具有遍历功能,baseFor
就是 createBaseFor 返回的函数,只用来赋值接收baseForOwn
是专门用来为 baseFor 赋值(其实就是没有 formRight 参数),规定了遍历集合,遍历集合回调和遍历集合的属性集合(这里用的是 Object.keys)createBaseEach
方法专门用来生成遍历指定集合,可遍历类数组或通常可遍历对象baseEach
或者是 baseEachRight
就是 createBaseEach 暴露出来的最终接口,createBaseFor,createBaseEach 都属于基函数,且这里面每一个函数都 pure
将数组扁平化一次,这里的扁平化一次代表的是将深度扁平一次
console.log(Lodash.flatten([1,2,[3,[4]],5,[6]])) //=>[ 1, 2, 3, [ 4 ], 5, 6 ]
#
baseFlatten 私有方法该方法在 flatten 方法之上,用递归的方式实现了多层次的扁平化, flatten 可以说只是这个方法的一个接口,该方法可以接收五个参数
参数一需要扁平化的数组,参数二需要扁平化有层次,参数三是一个回调,这个回调在 lodash 源码中默认表示判断是否是一个数组或类数组,参数四是一个用于判断是否需要跳过参数三回调的检查,而参数五可以是一个初始化参数,像 Array.prototype.reduce 方法一样
function _baseFlatten(array, checkCallback, result) {
var index = -1
checkCallback = checkCallback || Array.isArray
result = result || [] // 默认初始值为空数组
while ( ++index < array.length ) {
var value = array[index]
if( checkCallback(value) ){ // 为数组
// 特别注意此处应该 array 就应该为 value 这个数组
_baseFlatten(value, checkCallback, result)
} else {
result.push(value)
}
}
return result
}
console.log( _baseFlatten([1,2,[3,4],5]) ) //=> [ 1, 2, 3, 4 , 5 ]
console.log( _baseFlatten([1,2,[3,4],5]) ) //=> [ 1, 2, 3, 4 , 5 ]
console.log( _baseFlatten([1,2,[3,4],5], Array.isArray, [100]) ) //=> [ 100, 1, 2, 3, 4, 5 ]
这是一个简单版的 baseFlatten, 可接收四个参数可以实现一个简单的扁平化,也可以,在当前方法中添加一个深度标识或再添加上一个 Lodash.#
baseFlatten 方法的一个 isStrict 标识,再对上述方法改造
function _baseFlatten(array, depth, checkCallback, result) {
var index = -1,
length = array.length
checkCallback || ( checkCallback = Array.isArray )
result || ( result = [])
while ( ++index < length ) {
var value = array[index]
if( depth && checkCallback(value)){ // 如果有深度
if( depth > 1 ){ // 深度大于 1 并且为数组
_baseFlatten(value, depth - 1, checkCallback, result)
} else {
// !!! 当深度超过 0 次,需要将当前是数组的元素添加到 result 中
// 两种方法 concat, 还有一种就是 apply
result = result.concat(value)
}
} else {
result.push(value)
}
}
return result
}
console.log( _baseFlatten([1,2,[3,4],5], 0) ) //=> [ 1, 2, [ 3, 4 ], 5 ]
console.log( _baseFlatten([1,2,[3,4],5], 1, Array.isArray) ) //=> [ 1, 2, 3, 4 , 5 ]
console.log( _baseFlatten([1,2,[3,4],5], 1, null, [200]) ) //=> [ 200, 1, 2, 3, 4, 5 ]
再递归后的第一次最需要注意的是,也是整个扁平化的关键点,因为当一个元素是数组时,该元素上有可能也有数组,也可能没有数组,而如果是扁平第一次,就说明该元素是一个数组才扁平,而关键就是将这整个数组在当次扁平时加入到结果中,而实现整个数组加入到另一个数组中有 concat 和 apply 两个方法,下面来看 lodash 中是怎么实现该方法的,源码如下:
function _isFlattenable(value) {
return Lodash.isArray(value) || Lodash.isArguments(value);
}
function _baseFlatten(array, depth, predicate, isStrict, result) {
var index = -1,
length = array.length;
// 参数整理
predicate || (predicate = _isFlattenable);
result || (result = []);
while (++index < length) {
var value = array[index];
// 当深度大于 0 且回调返回 true 时
// 这里的回调是默认等于 _isFlattenable 也就是判断是否为数组或类数组
if (depth > 0 && predicate(value)) {
// 如果超过2个深度,则递归调用自己,并且每一次之后深度都会减少一次,达到扁平
if (depth > 1) {
// Recursively flatten arrays (susceptible to call stack limits).
_baseFlatten(value, depth - 1, predicate, isStrict, result);
} else {
// arrayPush(result, value)
[].push.apply(result, value); // apply 到数组中
}
} else if (!isStrict) { // 如果为真,跳过也跳过回调检查,直接返回
result[result.length] = value; // 其实这里也是一个 push 操作
}
}
return result;
}
console.log( _baseFlatten([1,2,[3,[4,5],6],7,8,[9]], 1, _isFlattenable, false, [10, [20] ] ) )
//=> [ 10, [ 20 ], 1, 2, [ 3, [ 4, 5 ], 6 ], 7, 8, [ 9 ] ]
console.log( _baseFlatten([1,2,[3,[4,5],6],7,8,[9]], 2, _isFlattenable, false ) )
//=> [ 1, 2, 3, 4, 5 , 6, 7, 8, 9 ]
console.log( _baseFlatten([1,2,[3,[4,5],6],7,8,[9]], 2, _isFlattenable, true ) )
//=> [ 4, 5 ]
console.log( _baseFlatten([1,2,[3,[4,5],6],7,8,[9]], 0, _isFlattenable ) )
//=> [ 1, 2, [ 3, [ 4, 5 ], 6 ], 7, 8, [ 9 ] ]
从源码中两个或运算看整理参数也是与通常不同,或运算也算是一个表达式,与通常的 result = result || []
这样写法不同,但实际结果相同,这是因为或运算有个特点,也就是如果表达式为真则直接返回左操作数,如果为假则会返回右操作数,这里右操作数是一个赋值操作,而赋值运算也会返回一个结果,如果在 apply 和最后中括号赋值直接在内部操作,可能该方法会变纯
还有个较巧妙的地方就是 isStrict 参数,先来看看可以做什么,如果将 isStrict 设置为 true,那么结果就只会将指定深度的元素合并,如下试例:
console.log(
_baseFlatten(
[1,2,[3,[4,5],6],[7,8,[9]]],
2, _isFlattenable, true
)
)
//=> [ 4, 5, 9 ]
每遍历出来的元素要么是一个原始值,要么又是一个类数组,如果是类数组则会递归调用,但每一次的最后结果都是一样,会将每个原始值连接起来,这个时候 isStrict 参数就出现了作用
arrayPush
将是类数组追加到末尾所以如果需要扁平的数组已经是被扁平后的数组,这个时候将 isStrict 设置为 true 结果肯定是一个空数组
console.log( _baseFlatten([1,2,3,4,5,6,7,8,9], 1, _isFlattenable, true) )
//=> []
由 _baseFlaten
可知道影响扁平深度的是一个 depth 计数变量,而 flattenDeep 则是_baseFlatten
暴露出来深度扁平的方法,而其中的 depth 则是一个 INFINITY
,但需要请注意的是 INFINITY 是不能直接被字面量表示的,在 loadsh 中用 1/0
表达示得到了 INFINITY,flattenDeep 会一直扁平化数组到元素中没有数组为止
console.log( Lodash.flattenDeep([1,2,3,4,[5,6,7,8,[9,10,11,12,13,[14,15,16,17]]]]))
//=> [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 ]
对于 ES5 提供的一些方法,比如 Array.prototype.filter,Array.prototype.map,Array.prototype.reduce Lodash 都是有实现的,如果浏览器不支持 ES5, lodash 则是比较好的选择
console.log( Lodash.filter ) //=> [Function: filter]
console.log( Lodash.forEach ) //=> [Function: forEach]
console.log( Lodash.every ) //=> [Function: every]
console.log( Lodash.map ) //=> [Function: map]
console.log( Lodash.reduce ) //=> [Function: reduce]
Lodash.forEach 方法是由 #baseEach()
私有方法生成的
回忆一下 #baseEach()
,由 #createBaseEach(baseForOwn)
和参数 baseForOwn
两个函数生成,而 baseForOwn 又是由 createBaseFor(false)
生成,此处最后则生成了 forEach,一个最终的实现接口
let foo = [1,2,3,4,5,6]
let bar = Lodash.forEach(foo, function(i,v){})
console.log(foo)//=> [ 1, 2, 3, 4, 5, 6 ]
console.log(bar)//=> [ 1, 2, 3, 4, 5, 6 ]
可以接收两个参数,参数一需要被迭代的集合,参数二则是一个迭代回调, #baseEach()
本身就是由 createBaseEach 生成, createBaseEach 就是可迭代类数组或对象,而迭代回调方法则可自定义,为什么这么说呢?
#
baseIterateelodash 解释该私有方法是主要是实现 Lodash.iteratee
, forEach 也是由它生成的,以下是它的伪源码:
// 可遍历的本体函数
// 可以想成是一个默认的遍历回调,就是返回遍历值
function _identity(value) {
return value;
}
// 默认回调2
function _baseMatches(source) {
var props = nativeKeys(source);
return function(object) {
//...
return true;
};
}
// 默认回调3
function _baseProperty(key) {
return function(object) {
//...
};
}
// 生成一个默认回调方法
function _baseIteratee(func) {
// 自定义直接返回
if (typeof func == 'function') {
return func;
}
// 如果没有则直接返回本体回调
if (func == null) {
return identity;
}
// 如果是对象或是其它则对应返回回调2和3
return (typeof func == 'object' ? _baseMatches : _baseProperty)(func);
}
function forEach(collection, iteratee) {
return baseEach(collection, _baseIteratee(iteratee));
}
也就说, baseIteratee 在这里是为了为 baseEach 参数二回调进行一个处理,这也是为什么在 Lodash.iteratee 参数二可以是一个函数,一个对象,一个数组等
#
baseMatches 与 #
baseProperty该回调作用很明确,第一步遍历可枚举的属性与目标对象的每个属性和值是否相等,也是Lodash.metches
的基函数
而取得对象的所有属性名用的是可枚举对象属性名的 Object.keys,得到目标对象的一个可枚举属性数组,在通常的 Object.keys 之上,在 ES5 里,Object.keys 参数不是对象(而是一个原始值),那么它会抛出 TypeError。 在 ES2015 中,非对象的参数将被强制转换为一个对象。
const _nativeKeys = function overArg(func, transform) {
return function _overArg_inner_(arg) {
return func(transform(arg));
};
}(Object.keys, Object)
console.log( _nativeKeys({'name': 'qlove', 'age': 10}) )
第二步:
不是对象时比较相等不再是像通常直接foo[prop] == bar[prop]
这样,lodash 是一个函数工具库,作为回调2#baseIsEqual()
,baseIsEqual 之后会详细解释,下面是 baseMatches 回调2源码:
function _baseMatches(source) {
var props = nativeKeys(source);
return function _baseMatches_inner_(object) {
var length = props.length;
if (object == null) {
return !length;
}
object = Object(object);
while (length--) {
var key = props[length];
if (!(key in object &&
baseIsEqual(source[key], object[key], COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG)
)) {
return false;
}
}
return true;
};
}
与之相反,如果不是对象,则直接用属性名取值,作为回调3的判断
function _baseProperty(key) {
return function(object) {
return object == null ? undefined : object[key];
};
}
最后附上整个 baseIteratee 源码:
function baseIteratee(func) {
if (typeof func == 'function') {
return func;
}
if (func == null) {
return identity;
}
return (typeof func == 'object' ? baseMatches : baseProperty)(func);
}
如果参数为回调,会直接返回该回调函数生成一个由传入回调的回调迭代函数,如果是一个对象,这的对象不局限只是 object 任何 typeof 返回的对象都可以,如果为对象则会返回默认的 baseMatches 方法作为返回的回调迭代函数,该函数作用就是判断与两个对象值是否相等,而 baseProperty 是接收一个不是对象的类 Key,最终会返回调用该回调迭代的指定对象的该 key 值
#
baseIsEqual - - - 位掩码标识位运算的几个规则:
&
, 两个数二进制位都为 1 则结果为 1, 否则为 0|
, 两个数二进制位有一个为 1 则结果为1,否则为 0~
, 一个数的 1 与 0 位互换^
, 两个数二进制位,相互为 1 或相互为 0 则为 0, 否则为 1<<
, 将左操作数的二进制位依次向左移动右操作数位数(溢出的位数用0补充),取最后结果>>
, 将左操作数的二进制位依次向右移动右操作数位数(演出的位数用0补充),取最后结果>>>
, 将左操作数的二进制位依次向右移动,如果出现溢出,则不算作符号位,直接算作正常位,取最后结果记住以上的计算规则,位掩码会用上
这里借网上一个例子,这个例子不是什么1000 瓶毒药多个老鼠可以尝出那瓶毒药,不研究算法,是一个关于权限的例子
假设有这样一个系统,有四个权限,查看,更新,删除,新增四个权限,有许多用户,每个用户分配了一个权限ID,这个ID就包含了四个权限可用值如何确定这个ID包含了那些权限?
当只允许有一个权限时:
const CAN_SELECT = 1 // 可以修改权限 十进行 1 二进行 1 << 0 == 0001
const CAN_INSERT = 2 // 可以新增权限 十进行 2 二进行 1 << 1 == 0010
const CAN_DELETE = 4 // 可以新增权限 十进行 4 二进行 1 << 2 == 0100
const CAN_UPDATE = 8 // 可以新增权限 十进行 8 二进行 1 << 3 == 1000
// 权限变量
// 可以是指定的一个权限
// 也可以是几个权限组合
// 首先确定只有一个权限
let auth = 1
// 抓住与运算特点,我们要看一个数是否满足另一个数
// 也就是两个数的二进制位有相等的位,且在并不改变原位数的值
// 只有与运算,两个数二进制位都为 1 则结果为 1, 否则为 0
// 0001 & 0001 => 0001
// 两个操作数相等
console.log( (auth & CAN_SELECT) != 0 ) //=> true
// 0001 & 0100 => 0000
console.log( (auth & CAN_UPDATE) != 0 ) //=> false
// 改变权限
auth = 4
console.log( (auth & CAN_SELECT) == CAN_SELECT ) //=> false
console.log( (auth & CAN_DELETE) == CAN_DELETE ) //=> true
当可以有多个权限时:
auth = 3
// 0011 & 0001 => 0001
console.log( (auth & CAN_SELECT) != 0 ) //=> true
// 0011 & 0010 => 0010
console.log( (auth & CAN_INSERT) != 0 ) //=> true
四种权限有 1 6种组合方式,这16种组合方式就都是通过位运算得来的,其中参与位运算的每个因子你都可以叫做掩码MASK
,所以还可以这样看是否有删除或更新权限
auth = 7
// 删除和更新权限分别是 4 和 8
// 是否有其中一个权限,就可以看这个两个权限是否包含
// 首先, 可以直接从十进制看出只要一个数大于4就表示了有两个权限的其中一个
// 但是不一定两个比较顺序是否一致
// 或者是将两个权限相加再用与运算不就可以了
// 0100 | 1000 => 1100
// 1110 & 1100 => 1100
console.log( (auth & (CAN_DELETE | CAN_UPDATE)) != 0 ) //=> true
const CAN_DELETE_UPDATE = CAN_DELETE | CAN_UPDATE
console.log( !! (auth & CAN_DELETE_UPDATE) ) //=> true
如上 CAN_DELETE_UPDATE 在程序上就叫掩码,就是位掩码
如果当需要权限必须满足时呢?其实,仔细的话就会发现上面的 与运算后的结果要么为0要么不为0,为0则表示被包含在其中,而如果不等于0且等掩码不就表示必须要有这两个权限了
let auth = 6
const CAN_DELETE_UPDATE = CAN_DELETE | CAN_UPDATE
console.log( (auth & CAN_DELETE_UPDATE) == CAN_DELETE_UPDATE ) //=> false
auth = 12
console.log( (auth & CAN_DELETE_UPDATE) == CAN_DELETE_UPDATE ) //=> true
当然十六进制的话表示的更多,其它的八进制都可以,但它们的思想是一样的,最后借网上一个总结增加其它的位操作方法如下:
增加属性 |
如果需要向flag变量中增加某个FLAG,使用 |
运算符 flag |= XXX_FLAG
原因: 如果flag变量没有XXX_FLAG,则|
完后flag对应的位的值为1,如果已经有XXX_FLAG,则|
完后值不会变,对应位还是1。
包含属性 &
如果需要判断flag变量中是否包含XXX_FLAG,使用"&"运算符,flag & XXX_FLAG != 0 或者 flag & XXX_FLAG = XXX_FLAG。
原因: 如果flag变量里包含XXX_FLAG,则&
完后flag对应的位的值为1,因为XXX_FLAG的定义保证了只有一位非0,其他位都为0,所以如果是包含的话进行&
运算后值不为0,该位上的值为此XXX_FLAG的所在位上的值,不包含的话值为0。
去除属性 &~
如果需要去除flag变量的XXX_FLAG, 使用 flag &= ~XXX_FLAG
原因: 先对XXX_FLAG进行取反则XXX_FLAG原来非0的那一位变为0,然后使用&
运算后如果flag变量非0的那一位变为0,则意味着flag变量不包含XXX_FLAG
#
baseIsEqual上述的回调2方法中,就用位掩码操作,虽然在核心中不多,但是在完整的 lodash 中丰富的们拉掩码操作
当值不相等,并且 value 和 other 都 likeObject, 则需要深度比较, 而在第三个参数会传入 COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG
拉掩码, baseIsEqual lodash 解释的是 1 是无序比较, 2 是部分比较, 如果进行或运算就是 3, #baseIsEqualDeep
,该方法很长,从接收到参数看起,该方法是可以接收6个参数的,对此处而言,将形参转换成如下:
当 baseIsEqualDeep 所有形参从 baseIsEqual 传入,第一步处理参数,区分操作数是数组还是对象,如果是纯对象还需要用 toString 获取出具体是什么对象,直接比较出结果为止
确实很长, forEach 由私有方法 baseEach 生成, baseEach 是由私有方法#createBaseEach(baseForOwn)
和参数 baseForOwn
两个函数生成,生成 forEach 时,的迭代回调可以是自定义,如果没有则加用默认迭代回调,而默认回调基本上有三个,一个是 identity,
baseMatches, baseProperty 三个回调构成
如果当自定义迭代回调不存在,则会直接是 identity 返回本身值,如果如果是对象或者是类数组则会分别用 baseMatches 和
baseProperty, baseMatches 可以深度比较,当然这里最重要的不是什么底层实现,也不是 equalArrays,equalByTag,equalObjects 其中的谁谁生成了谁,而是位掩码这种思想,关于 baseIsEqual 或是 baseIsEqualDeep 之后会详细分析源码
tap 属于一个链式调用方法,这里也是一个可以操作数组的方法, lodash 解释就调用一个拦截器并返回原值,作用就是为链式操作做准备,就像是一个请求拦截器,在请求完发送之前处理的方法,这个方法也是如果,但该方法可以这样说,是目前为止核心中最简单的一个方法,源码就是为一个值参数一个回调,然后返回作为参数的原值,代码层面上简单,但思想独特
function _tap(value, interceptor){
interceptor(value)
return value
}
let bar = [1,2,3]
let foo = _tap(bar, function(array){
array.pop()
})
console.log( bar === foo)
console.log(foo.pop()) //=> 2
与 Lodash.tap 类似,其返回的并不是原值,而是被拦截器的返回值,如果一个原值是数组,被 thru, 拦截器返回一个对象,则 thru 最后就会返回对象
let bar = [1,2,3]
function _thru(value, interceptor){
return interceptor(value)
}
let foo = _thru(bar, function(array){
return { 'bar' : array};
})
console.log( foo ) //=>{ bar: [ 1, 2, 3 ] }
类似这样的方法还有很多,其实主要就是对函数或数据类型做一个包装,支持函数式的操作
max 寻找数组中最大的值,min 寻找数组中最小的值,想象有一串数字,不借用 max 方法找到最大的值,通常方法,遍历一次这串数字,取其中一个数字与所有数字进行比较,如果出现比该数字还大的数字则赋值成较大的数字,依次遍历到最后一个数字,从语言层面上来说就是循环或是递归,当然通常办法也只有这样。
下面就先来了解几个方法
#
baseGt该方法接收两个参数,判断两个一个数是否大于另一个数
function _baseGt (value, other) {
return value > other
}
console.log(_baseGt(2, 1)) //=>true
#
baseLt该方法接收也两个参数,判断两个一个数是否小于另一个数
function _baseLt (value, other) {
return value < other
}
console.log(_baseGt(2, 1)) //=> false
可能会有不解,为什么一个大于或是小于都要单独写个方法来,前面的比较两个数相等也要专门写个 eq 方法来比较直接用==,>,<
这样的运算符它不香吗?
它还真有点不香,这个就要从表达式(expression)和语句(statement)说起,具体的话可参考 ecma表达式规范 和 ecma语句规范
function(){}//报错
(function(){})//不报错
function f(x){ return x + 1 }()//报错
function f(x){ return x + 1 }(1)//不报错,为什么返回 1
()
里的当做表达式去解析,在这里就会当做匿名函数表达式解析,所以不会报错()
会被当做分组操作符,分组操作符里必须要有表达式,所以这里报错(1)
,仅仅是相当于在声明语句之后又跟了一条毫无关系的表达式函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。
当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。来自百度百科
举个非常简单的例子,有许多数字需要进行大于比较,每两个数字之间进行比较就会产生一个操作, >
或<
操作,代码就是为了简便人们方便计算,简单的来说就是让人们偷懒,将多个操作换成一个操作不香吗?错误也会少很多呀,当然这只是个玩笑话。
函数的计算往往比指令的执行重要且函数的计算可随时调用
#
baseExtremum极值函数基函数,主要实现 Lodash.max 和 Lodash.min,如果说有了 baseGt 和 baseLt 让我来实现 baseExtremum 我会像下面这样去实现它
function _baseLt(value, other) {
return value < other
}
function _baseGt(value, other) {
return value > other
}
function _max(array) {
let i = 0,
len = array.length,
result = array[0]
while (++i < len) {
result = _baseGt(array[i], result) ? array[i] : result
}
return result
}
console.log(_max([2,3,4,8,5,2,6]))//=> 8
类似的 min 方法也可以这样得到,可_baseGt
和_baseLt
确实独立存在且是个纯函数,_max
也独立存在,但是好像该方法除了算数组最大的值好像就没有其它用途了,与其这样还不如直接将方法内部的_baseGt
直接写成>
操作符,就当成一个普通的工具函数企不是一样的,也确实可以这样最大值,最小值,中间值…就分别写个方法嘛也是一样的,能偷懒就偷懒有时候太老实也不是一件很好的事
何不将一系列操作直接嵌套成一个函数调用呢,下面来看看 lodash 中的这个一操作的函数源码
function baseExtremum(array, iteratee, comparator) {
var index = -1,
length = array.length;
while (++index < length) {
var value = array[index],
//先走一遍迭代回调,可以将操作前对操作数作其它操作
current = iteratee(value);
if (current != null && (computed === undefined
? (current === current && !false)
: comparator(current, computed) // 一系列的操作
)) {
var computed = current,
result = value;
}
}
return result;
}
lodash 解释 主要生成 _.max
和_.min
这样的方法,用comparator
确定极值,参数一是一个目标数组,参数二是一个迭代回调,之前的 each 等许多地方都用上了迭代回调,参数三就是这里的一系列操作
function _baseExtremum(array, iteratee, comparator) {
var index = -1,
length = array.length;
while (++index < length) {
var value = array[index],
//先走一遍迭代回调,可以将操作前对操作数作其它操作
current = iteratee(value);
if (current != null && (computed === undefined
? (current === current && !false)
: comparator(current, computed) // 一系列的操作
)) {
var computed = current,
result = value;
}
}
return result;
}
function _baseLt(value, other) {
return value < other
}
function _baseGt(value, other) {
return value > other
}
function _identity (value) {
return value
}
function _max(array) {
return (array && array.length)
? _baseExtremum(array, identity, _baseGt)
: undefined;
}
function _min(array) {
return (array && array.length)
? _baseExtremum(array, identity, _baseLt)
: undefined;
}
console.log( _max([1,2,4,5,8,7,3]))//=> 8
console.log( _min([1,2,4,5,8,7,3]))//=> 1
借用_baseExtremum
生成_max
和_min
,每个方法独立,从代码层面上看逻辑清晰,高可用,稳定每个方法都无副作用
console.log( Lodash.join([1,2,3,4], ',') ) //=> 1,2,3,4
console.log( [1,2,3,4].join(',') )//=> 1,2,3,4
console.log( '1,2,3,4'.split(',') )//=> [ '1', '2', '3', '4' ]
console.log( Lodash.split('1,2,3,4', ',') )//=> [ '1', '2', '3', '4' ]
第一个 lodash 成员都会有自己的属性,方法,这样的设计与大多数都一样,只是在 lodash 中,个人认为,大多地方不在是以数据为单位,就像 jQuery 以每个 dom 元素作为单位, lodash 以一个函数作为一个单位,主要围绕着函数
内部用私有方法 baseEach 为 pop, join, replace, reverse, split, push, shift, sort, splice, unshift 这些字符串数组方法扩展,其方法原型基本都是来自 String.prototype 或 Array.prototype
凡带有#号开头方法表示为私有方法,这一点遵循 ECMAScript 2019