引入问题
如果有题目让实现的concat方法,那么你会想到怎么做?
基本实现
首先最简单的实现方式
function oConcat() {
let args = arguments, result = [];
for(let arg of args) {
if(Array.isArray(arg)) {
result.push(...arg);
}else {
result.push(arg);
}
}
return result;
}
这样可以满足基本需求,但依然存在问题,Symbol.isConcatSpreadable对原生concat功能的增强将无法适用于我们自己写的oConcat方法
concat功能增强
我们可以立马想到,对包含Symbol.isConcatSpreadable的对象,特殊处理,手工遍历内部属性,放到result中
function oConcat() {
let args = arguments, result = [];
for(let arg of args) {
if(Array.isArray(arg)) {
result.push(...arg);
}else if( Object.getOwnPropertySymbols(arg).indexOf(Symbol.isConcatSpreadable) != -1 && arg[Symbol.isConcatSpreadable]){
for(let i = 0; i < arg.length; i ++) {
result.push(arg[i]);
}
}else {
result.push(arg);
}
}
return result;
}
但是数组类型和Symbol.isConcatSpreadable对象他们都是可展开的对象,可不可以对其统一处理,设计一个数组展开flatten的功能实现。
相比较于Array.concat,lodash.concat还支持arguments对象的连接,我们可不可以添加对arguments对象的连接?
_.concat 实现
实现注意点
判断对象是否可展开: Array对象,对象中Symbol.isConcatSpreadable = true, arguments对象
考虑到flatten方法,concat实现可以简化为arguments.slice(1)的参数通过flatten方法展开一层之后放到arguments[0]中
判断是否为arguments对象,使用Object.prototype.toString.call方法,但是需要考虑 Symbol.toStringTag手工修改toString返回值情况
获取对象原始类型
手动排除Symbol.toStringTag对对象原型的影响,获取对象属性
function getRowTag(value) {
let isOwn = Object.prototype.hasOwnProperty.call(value,Symbol.toStringTag), tag = value[Symbol.toStringTag];
let unmasked = false;
try{
//将对象上Symbol.toStringTag重置或者覆盖原型上的Symbol.toStringTag
value[Symbol.toStringTag] = undefine;
unmasked = true;
}catch(e) {}
//移除属性后获取真正的类型
let result = Object.prototype.toString.call(value);
if(unmasked) {
if(isOwn) {//如果是本身属性,则复原,否则直接删除此属性
value[symToStringTag] = tag;
}else {
delete value[Symbol.toStringTag];
}
}
}
function baseGetTag(value) {
if(value == null) { //排除null和undefined的影响
return value === undefined ? '[object Undefined]' : '[object Unll]';
}
//如果对象中存在Symbol.toStringTag属性的话,需排除,否则直接使用Object.prototype.toString
(Symbol.toStringTag && Symbol.toStringTag in Object(value)) ?
getRowTag(value)
: Object.prototype.toString.call(value);
}
flatten扁平化
数组扁平化已经是ES的表转数组API了,这里Lodash内部的实现如下:
//检测对象是否可以展开 Array, Arguments,包含Symbol.isConcatSpreadable的对象
function isFlattenable(value){
return value && (Array.isArray(value) || isArguments(value) || (Symbol.isConcatSpreadable && value[Symbol.isConcatSpreadable] ))
}
function baseFlatten(array, depth, isStrict, result){
let index = -1, length = array.length;
result || (result = []);
while(++index < length) {
let value = array[index];
if(depth > 0 && isFlattenable(value)) { //这里默认使用isFlattenabe,可以作为参数传入,定制过滤条件
if(depth > 1) {
baseFlatten(value, depth-1);
}else {
arrayPush(result, value); //可展开,但是不继续深度展开,则将对象插入
}
}else if(!isStrict){ //是否将不符合条件的isFlattenable或者具体展开的内容放入结果
result[result.length] = value;
}
}
return result;
}
检测Arguments对象
检测Arguments对象,默认通过Object.prototype.toString方法获取,需要考虑Symbol.toStringTag对toString方法的影响
Lodash另一种判断Argumsnts对象的方法,检测对象中是否存在callee,并且callee属性isEmumerable为false
function baseIsArguments(value) {
return typeof value == 'object' && baseGetTag(value) == '[object Arguments]';
}
const isArguments = baseIsArguments(function(){ return arguments }()) ? baseIsArguments : (value) => {
return value && typeof value == 'object' && Object.prototype.hasOwnProperty.call(value, "callee") && Object.prototype.propertyIsEnumerable(value, "callee")
}
最终实现
function arrayPush(array, value){
let length = value.length, offset = array.length, index = -1;
while(++index < length) {
array[offset+index] = value[index];
}
return array;
}
function copyArray(source, array){
let length = array.length,index = -1;
array || (array = Array(length));
while(++index < length) {
array[index] = array[index];
}
return array;
}
function getRowTag(value) {
let isOwn = Object.prototype.hasOwnProperty.call(value,Symbol.toStringTag), tag = value[Symbol.toStringTag];
let unmasked = false;
try{
value[Symbol.toStringTag] = undefine;
unmasked = true;
}catch(e) {}
let result = Object.prototype.toString.call(value);
if(unmasked) {
if(isOwn) {
value[symToStringTag] = tag;
}else {
delete value[Symbol.toStringTag];
}
}
}
function baseGetTag(value) {
if(value == null) {
return value === undefined ? '[object Undefined]' : '[object Unll]';
}
(Symbol.toStringTag && Symbol.toStringTag in Object(value)) ? getRowTag(value) : Object.prototype.toString.call(value);
}
function baseIsArguments(value) {
return typeof value == 'object' && baseGetTag(value) == '[object Arguments]';
}
const isArguments = baseIsArguments(function(){ return arguments }()) ? baseIsArguments : (value) => {
return value && typeof value == 'object' && Object.prototype.hasOwnProperty.call(value, "callee") && Object.prototype.propertyIsEnumerable(value, "callee")
}
function isFlattenable(value){
return value && (Array.isArray(value) || isArguments(value) || (Symbol.isConcatSpreadable && value[Symbol.isConcatSpreadable] ))
}
function baseFlatten(array, depth, isStrict, result){
let index = -1, length = array.length;
result || (result = []);
while(++index < length) {
let value = array[index];
if(depth > 0 && isFlattenable(value)) {
if(depth > 1) {
baseFlatten(value, depth-1);
}else {
arrayPush(result, value);
}
}else if(!isStrict){
result[result.length] = value;
}
}
return result;
}
function oConcat() {
let length = arguments.length;
if(!length) {
return [];
}
let array = arguments[0], index = length, args = Array(length-1);
while(index--) {
args[index-1] = arguments[index];
}
return arrayPush(Array.isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));
}
TS对concat的思考
TS中 @types/lodash对concat的定义如下
type Many<T> = T | ReadonlyArray<T>; //ReadonlyArray包含Array.prototype中需改数组之外的方法
concat<T>(...values: Array<Many<T>>): T[]; //接收对象仅限于一种类型,并且只能是当前类型的变量或者数组
也就表示,在concat给出的例子中, _.concat(array, 2, [3], [[4]]) 将报错,因为不支持传入两层的数组
es5中Array.concat定义如下
//和lodash类似
interface ConcatArray<T> {
readonly length: number;
readonly [n: number]: T;
join(separator?: string): string;
slice(start?: number, end?: number): T[];
}
concat(...items: ConcatArray<T>[]): T[];
concat(...items: (T | ConcatArray<T>)[]): T[];
同样,更加规范了数组的使用
如果按照TS规范使用concat,规范性更强,无需考虑concat中对象是否可遍历等内容