由_.concat引发的思考

488 阅读2分钟

引入问题

如果有题目让实现的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.toStringTagtoString方法的影响

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中对象是否可遍历等内容