从Array.from到Symbol.iterator

90 阅读3分钟

关于Array.from的实现,MDN是这么说的, image.png 这句话的意思是:

  1. 这是Array的静态方法
  2. 用于浅拷贝类数组、数组或者可迭代对象
  3. 返回一个新数组实例。

该方法接收三个参数,其中,后两个是可选参数,分别是

  1. 可迭代对象(像Map或者Set)或者类数组(拥有length属性的对象)
  2. map函数,类似于Array.prototype.map
  3. 绑定的this对象

关于Array.from的用法,可以做个测试

数组对象
image.png

类数组对象
image.png

可迭代对象
image.pngimage.png

带表达式函数的
image.png

接下来看一下Array.from的源码实现:

if (!Array.from) {
    Array.from = (function () {
        var symbolIterator;
        try {
            symbolIterator = Symbol.iterator
                ? Symbol.iterator
                : 'Symbol(Symbol.iterator)';
        } catch (e) {
            symbolIterator = 'Symbol(Symbol.iterator)';
        }

        var toStr = Object.prototype.toString;
        var isCallable = function (fn) {
            return (
                typeof fn === 'function' ||
                toStr.call(fn) === '[object Function]'
            );
        };
        var toInteger = function (value) {
            var number = Number(value);
            if (isNaN(number)) return 0;
            if (number === 0 || !isFinite(number)) return number;
            return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
        };
        var maxSafeInteger = Math.pow(2, 53) - 1;
        var toLength = function (value) {
            var len = toInteger(value);
            return Math.min(Math.max(len, 0), maxSafeInteger);
        };

        var setGetItemHandler = function setGetItemHandler(isIterator, items) {
            var iterator = isIterator && items[symbolIterator]();
            return function getItem(k) {
                return isIterator ? iterator.next() : items[k];
            };
        };

        var getArray = function getArray(
            T,
            A,
            len,
            getItem,
            isIterator,
            mapFn
        ) {
            // 16. Let k be 0.
            var k = 0;

            // 17. Repeat, while k < len… or while iterator is done (also steps a - h)
            while (k < len || isIterator) {
                var item = getItem(k);
                var kValue = isIterator ? item.value : item;

                if (isIterator && item.done) {
                    return A;
                } else {
                    if (mapFn) {
                        A[k] =
                            typeof T === 'undefined'
                                ? mapFn(kValue, k)
                                : mapFn.call(T, kValue, k);
                    } else {
                        A[k] = kValue;
                    }
                }
                k += 1;
            }

            if (isIterator) {
                throw new TypeError(
                    'Array.from: provided arrayLike or iterator has length more then 2 ** 52 - 1'
                );
            } else {
                A.length = len;
            }

            return A;
        };

        // The length property of the from method is 1.
        return function from(arrayLikeOrIterator /*, mapFn, thisArg */) {
            // 1. Let C be the this value.
            var C = this;

            // 2. Let items be ToObject(arrayLikeOrIterator).
            var items = Object(arrayLikeOrIterator);
            var isIterator = isCallable(items[symbolIterator]);

            // 3. ReturnIfAbrupt(items).
            if (arrayLikeOrIterator == null && !isIterator) {
                throw new TypeError(
                    'Array.from requires an array-like object or iterator - not null or undefined'
                );
            }

            // 4. If mapfn is undefined, then let mapping be false.
            var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
            var T;
            if (typeof mapFn !== 'undefined') {
                // 5. else
                // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
                if (!isCallable(mapFn)) {
                    throw new TypeError(
                        'Array.from: when provided, the second argument must be a function'
                    );
                }

                // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
                if (arguments.length > 2) {
                    T = arguments[2];
                }
            }

            // 10. Let lenValue be Get(items, "length").
            // 11. Let len be ToLength(lenValue).
            var len = toLength(items.length);

            // 13. If IsConstructor(C) is true, then
            // 13. a. Let A be the result of calling the [[Construct]] internal method
            // of C with an argument list containing the single item len.
            // 14. a. Else, Let A be ArrayCreate(len).
            var A = isCallable(C) ? Object(new C(len)) : new Array(len);

            return getArray(
                T,
                A,
                len,
                setGetItemHandler(isIterator, items),
                isIterator,
                mapFn
            );
        };
    })();
}

关于这个源码我们拆开来看

  1. Array.from最后执行from,from返回getArray。先看from方法。
    1. 判断第一个参数是不是数组、类数组或者可迭代的对象。首先该参数不能为null或者undefined,其次该参数是可以迭代的。判断是否可迭代,就看能否通过for...of循环,for..of循环取决于Symbol.iterator属性,因此会有以下这一段
var isIterator = isCallable(items[symbolIterator]);

Symbol.iterator属性一般存在于可迭代对象的原型对象中,且是一个函数,这个函数是怎样的呢?文章最后面会有讲解,这里不是重点。

  1. 接下来获取map方法和this对象,因为可选,故可能不存在,因此做个容错处理
  2. 接下来获取对象的length,并新建一个数组
  3. 最后调用getArray方法
  4. 调用getArray方法。
    1. 获取每个迭代元素的值,如果是可以迭代的,就通过value属性获取,否则item就是元素的值。
    2. 可迭代的done属性如果为true,就停止并返回新数组;否则给新数组赋值。
    3. 给新数组赋值,要判断map函数是否存在,注意给map函数传递的参数只有两个,一个是每个元素值,一个是索引
    4. 最后返回新数组

这就是Array.from的源码解析,接下来说一下可迭代。
可迭代取决于Symbol.iterator方法。那么这个方法是怎样的呢?vue源码里面有这么一段:

if (hasSymbol && val[Symbol.iterator]) {
      ret = [];
      var iterator = val[Symbol.iterator]();
      var result = iterator.next();
      while (!result.done) {
        ret.push(render(result.value, ret.length));
        result = iterator.next();
      }
    }

那么我们可以推测一下,这个方法到底是什么样的。
js中纯对象是没有可迭代特性的,只能使用for..in但是不能使用for..of,我们可以尝试给对象赋值一个可迭代的属性并测试一下:

Object.prototype[Symbol.iterator] = function () {
    const obj = this;
    const keys = Object.keys(obj);
    let index = 0;
    return {
        next: function () {
            return {
                done: index === keys.length,
                value: obj[keys[index++]]
            }
        }
    }
}

const data = { name: '名称', value: '值', title: '标题' };
for (let item of data) {
    console.log(item);
}

结果如下:
image.png
所以这就是可迭代的逻辑了。