Skip to the code
cf.

Array.from

MediumPolyfills

Write Array.myFrom(items, mapFn, thisArg), which works like Array.from without calling it. It always returns a new plain array.

If items is iterable (it has a Symbol.iterator method, as arrays, strings, Sets, Maps, and generators do), the result holds the values its iterator produces, in order, even when items also has a length. Strings are iterated by code point, so a character made of two UTF-16 code units, like "π„ž", stays one element, and an array's holes come out as undefined.

Otherwise items is an array-like: the result has length elements, read from indexes 0 to length - 1, with undefined wherever an index is missing. A missing length means 0.

If mapFn is given, each element is replaced by mapFn(value, index), called with exactly those two arguments and with thisArg as its this. Throw a TypeError if items is null or undefined, or if mapFn is given (not undefined) but isn't a function.

Array.myFrom(new Set([1, 2, 2, 3]));          // [1, 2, 3]
Array.myFrom("aπ„ž");                           // ["a", "π„ž"]
Array.myFrom({ length: 3, 0: "x", 2: "z" });  // ["x", undefined, "z"]
Array.myFrom({ length: 3 }, (_, i) => i * 2); // [0, 2, 4]

What the tests check

  1. copies the values of an iterable
    expect(Array.myFrom(new Set([1, 2, 2, 3]))).toEqual([1, 2, 3]);
    expect(Array.myFrom(new Map([["a", 1]]))).toEqual([["a", 1]]);
    
  2. splits a string by code point
    expect(Array.myFrom("aπ„žb")).toEqual(["a", "π„ž", "b"]);
    
  3. uses the iterator, even when there's also a length
    function* letters() {
      yield "a";
      yield "b";
    }
    expect(Array.myFrom(letters())).toEqual(["a", "b"]);
    const both = {
      length: 5,
      *[Symbol.iterator]() {
        yield 1;
      },
    };
    expect(Array.myFrom(both)).toEqual([1]);
    
  4. makes a new, dense copy of an array
    const sparse = [1, , 3];
    const copy = Array.myFrom(sparse);
    expect(copy).not.toBe(sparse);
    expect(copy).toEqual([1, undefined, 3]);
    expect(1 in copy).toBe(true);
    
  5. reads an array-like by its length, with undefined for missing indexes
    const result = Array.myFrom({ length: 3, 0: "x", 2: "z" });
    expect(result).toEqual(["x", undefined, "z"]);
    expect(1 in result).toBe(true);
    expect(Array.myFrom({ 0: "ignored" })).toEqual([]);
    
  6. calls mapFn with the value and index, and thisArg as this
    const mapFn = spy(function (value, index) {
      return this.prefix + value + index;
    });
    expect(Array.myFrom(["a", "b"], mapFn, { prefix: "#" })).toEqual(["#a0", "#b1"]);
    expect(mapFn.calls[0].args).toEqual(["a", 0]);
    expect(Array.myFrom({ length: 3 }, (_, index) => index * 2)).toEqual([0, 2, 4]);
    
  7. throws a TypeError for null or undefined items
    expect(() => Array.myFrom(null)).toThrow(TypeError);
    expect(() => Array.myFrom(undefined)).toThrow(TypeError);
    
  8. throws a TypeError when mapFn is given but isn't a function
    expect(() => Array.myFrom([1], 42)).toThrow(TypeError);
    expect(() => Array.myFrom([], "double")).toThrow(TypeError);
    expect(() => Array.myFrom([], null)).toThrow(TypeError);
    expect(Array.myFrom([1], undefined)).toEqual([1]);
    
  9. works without the built-in Array.from
    const builtIn = Array.from;
    Array.from = function () {
      throw new Error("myFrom called the built-in Array.from");
    };
    let fromString;
    let fromArrayLike;
    try {
      // A string, not a Set or a Map: console.log shows those with Array.from.
      fromString = Array.myFrom("ab");
      fromArrayLike = Array.myFrom({ length: 1, 0: "c" });
    } finally {
      Array.from = builtIn;
    }
    expect(fromString).toEqual(["a", "b"]);
    expect(fromArrayLike).toEqual(["c"]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).