ES6及ESNext规范详解

1,330 阅读5分钟

1、ECMAScript规范发展简介

ECMAScript-history.jpg

  • ES6:指2015年6月发布的ES2015标准, 但是很多人在谈及ES6的时候, 都会把ES2016、ES2017等标准的内容也带进去
  • ESNext:泛指, 它永远指向下一个版本,如当前最新版本是ES2020, 那么ESNext指的就是2021年6月将要发布的标准

2、ES6常用API

1、let 和 const

    1. let引入了块级作用域的概念, 创建setTimeout函数时,变量i在作用域内。对于循环的每个迭代,引用的i是i的不同实例。
    1. 暂时性死区:不允许变量提升
    1. const就很简单了, 在let的基础上, 不可被修改
for(var i=0;i<=3;i++){
     setTimeout(function() { 
         console.log(i) 
     }, 10);
}  //4~4

原因:

  1. var定义的变量是全局的, 所以全局只有一个变量i。
  2. setTimeout是异步, 在下一轮事件循环, 等到执行的时候, 去找i变量的引用。所以函数找到了遍历完后的i, 此时它已经变成了4。

for(var i = 0; i <=3; i++) {
     // 通过自执行函数满足0,1,2,3
     (function (i) {
         setTimeout(function () {
             console.log(i);
         }, 10);
     })(i);
 }  //0~3

for(let i=0;i<=3;i++){  //通过作用域 let 满足0,1,2,3
     setTimeout(function() { 
         console.log(i) 
     }, 10);
}//0~3,let为块级作用域

2、箭头函数

    1. 箭头函数里的this是定义的时候决定的, 普通函数里的this是使用的时候决定的。
const teacher = {
    name: 'zhangsan',
    getName: function() {
        return `${this.name}`
    }
}
console.log(teacher.getName());     //zhangsan

const teacher = {
    name: 'zhangsan',
    getName: () => {
        return `${this.name}`
    }
}
console.log(teacher.getName());     //undefined
    1. 箭头函数不能被用作构造函数
    • 2.1 构造函数: 改变this指向到新实例出来的对象.
    • 2.2 箭头函数: this指向是定义的时候决定的.
    1. 简写箭头函数
const arrowFn = (value) => Number(value);

console.log(arrowFn('aaa'))

const arrowFn = () => {}
console.log(arrowFn)   //undefined

const arrowFn = () => ({})  //如果想得到对象,需要用括号包裹
console.log(arrowFn)   //{}

3、Class

    1. constructor:构造函数
    1. 可以使用set和get函数
    1. static为全局函数
    1. 直接使用变量即为类变量,无需声明
class Test {
    _name = '';
    constructor() {
        this.name = 'zhangsan';
    }

    static getFormatName() {
        return `${this.name} - xixi`;
    }

    get name() {
        return this._name;
    }

    set name(val) {
        console.log('name setter');
        this._name = val;
    }
}

console.log(new Test().name)    //name setter    zhangsan
console.log(Test.getFormatName())  //name setter   Test - xixi

4、模板字符串

支持变量和对象解析和换行

const b = 'test'
const a = `${b} - xxxx`;
const c = `我是换行
我换行了!
`;

面试题:编写render函数, 实现template render功能.

//要求
const year = '2021';
const month = '10';
const day = '01';
let template = '${year}-${month}-${day}';
let context = { year, month, day };
const str = render(template)({year,month,day});
console.log(str) // 2021-10-01

//实现:高阶函数(函数返回函数)
function render(template) {
 	return function(context) {
 		return template.replace(/${(.*?)}/g, (match, key) => context[key]);
 	} 
}
//.*表示:任意值
//?表示:匹配前面的表达式0或1个,或制定非贪婪限定符
//表达式 .* 就是单个字符匹配任意次,即贪婪匹配。 
//表达式 .*? 是满足条件的情况只匹配一次,即最小匹配.
//match: ${year} ${month} ${day}
//key: year month day

参考资料:replace的mdn资料

replace() 方法返回一个由替换值(replacement)替换部分或所有的模式(pattern)匹配项后的新字符串。模式可以是一个字符串或者一个正则表达式,替换值可以是一个字符串或者一个每次匹配都要调用的回调函数。如果pattern是字符串,则仅替换第一个匹配项。

str.replace(regexp|substr, newSubStr|function)

替换字符串可以插入下面的特殊变量名:

变量名代表的值
$$插入一个 "$"。
$&插入匹配的子串。
'$插入当前匹配的子串左边的内容。
$'插入当前匹配的子串右边的内容。
$n假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。提示:索引是从1开始。如果不存在第 n个分组,那么将会把匹配到到内容替换为字面量。比如不存在第3个分组,就会用“$3”替换匹配到的内容。
$<Name>这里Name 是一个分组名称。如果在正则表达式中并不存在分组(或者没有匹配),这个变量将被处理为空字符串。只有在支持命名分组捕获的浏览器中才能使用。

函数的参数:

变量名代表的值
match匹配的子串。(对应于上述的$&。)
p1,p2, ...假如replace()方法的第一个参数是一个RegExp对象,则代表第n个括号匹配的字符串。(对应于上述的11,2等。)例如,如果是用 /(\a+)(\b+)/ 这个来匹配,p1 就是匹配的 \a+p2 就是匹配的 \b+

5、解构

    1. 数组的解构
// 基础类型解构
let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1, 2, 3

// 对象数组解构
let [a, b, c] = [{name: '1'}, {name: '2'}, {name: '3'}]
console.log(a, b, c) // {name: '1'}, {name: '2'}, {name: '3'}

// ...解构
let [head, ...tail] = [1, 2, 3, 4]
console.log(head, tail) // 1, [2, 3, 4]

// 嵌套解构
let [a, [b], d] = [1, [2, 3], 4]
console.log(a, b, d) // 1, 2, 4

// 解构不成功为undefined
let [a, b, c] = [1]
console.log(a, b, c) // 1, undefined, undefined

// 解构默认赋值
let [a = 1, b = 2] = [3]
console.log(a, b) // 3, 2
    1. 对象的解构
// 对象属性解构
let { f1, f2 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2

// 可以不按照顺序,这是数组解构和对象解构的区别之一
let { f2, f1 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2

// 解构对象重命名
let { f1: rename, f2 } = { f1: 'test1', f2: 'test2' }
console.log(rename, f2) // test1, test2

// 嵌套解构
let { f1: {f11}} = { f1: { f11: 'test11', f12: 'test12' } }
console.log(f11) // test11

// 默认值
let { f1 = 'test1', f2: rename = 'test2' } = { f1: 'current1', f2: 'current2'}
console.log(f1, rename) // current1, current2
    1. 解构的原理 针对可迭代对象的Iterator接口,通过遍历器按顺序获取对应的值进行赋值。

1、什么是Iterator接口

Iterator是一种接口,为各种不一样的数据解构提供统一的访问机制。任何数据解构只要有Iterator接口,就能通过遍历操作,依次按顺序处理数据结构内所有的成员。使用for of的语法遍历数据结构时,自动寻找Iterator接口。

可迭代对象是Iterator接口的实现,有两个协议:可迭代协议和迭代器协议。

  • 可迭代协议:对象必须实现iterator方法,即对象或其原型链上必须有一个名叫Symbol.iterator的属性,该属性的值为无参函数,函数返回迭代器协议。
  • 迭代器协议:产生一个有限或无限序列值,必须实现next()方法,方法返回对象有donevalue属性。

2、Iterator有什么用

  • 为各种不同的数据解构提供统一的访问接口
  • 使得数据解构能按次序排列处理
  • 可以使用ES6最新命令 for of进行遍历
// 生成Iterator对象
function generateIterator(array) {
    let nextIndex = 0
    return {
        next: () => nextIndex < array.length ? {
            value: array[nextIndex++],
            done: false
        } : {
            value: undefined,
            done: true
        }
    };
}

const iterator = generateIterator([0, 1, 2])

console.log(iterator.next())    // {value: 0, done: false}
console.log(iterator.next())    // {value: 1, done: false}
console.log(iterator.next())    // {value: 2, done: false}
console.log(iterator.next())    // {value: undefine, done: true}

6、遍历

1、for-in

  • 遍历当前对象,还包括原型链上的非Symbol的可枚举属性
  • 不适合遍历数组,主要应用于遍历对象
  • 可与break,continue,return配合

缺点

  • for in 不仅会遍历当前对象,还包括原型链上的可枚举属性 Object.prototype
  • for in 不适合遍历数组,主要应用为对象
let obj = {
  'a': 'test1',
  'b': 'test2'
}
for (const key in obj){
  console.log(key, obj[key]) //遍历数组时,key为数值下标字符串;遍历对象,key为对象字段名
}

2、for-of

  • 可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments对象,NodeList对象)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。
  • 仅遍历当前对象
  • 可与break,continue,return配合
let arr = [{age: 1}, {age: 5}, {age: 100}, {age: 34}]
for(let {age} of arr) {
    if (age > 10) {
        break // for of 允许中断
    }
    console.log(age)
}

7、Object

  1. Object.keys

该方法返回一个给定对象的自身可枚举属性组成的数组。

const obj = { a: 1, b: 2 };
const keys = Object.keys(obj); // [a, b]
  1. Object.values 该方法返回一个给定对象自身的所有可枚举属性值的数组。
const obj = { a: 1, b: 2 };
const keys = Object.keys(obj); // [1, 2]
  1. Object.entries

该方法返回一个给定对象自身可枚举属性的键值对数组。

const obj = { a: 1, b: 2 };
const keys = Object.entries(obj); // [ [ 'a', 1 ], [ 'b', 2 ] ]
  1. Object.getOwnPropertyNames

该方法返回一个数组,该数组对元素是 obj自身拥有的枚举或不可枚举属性名称字符串。

Object.prototype.aa = '1111';

const testData = {
    a: 1,
    b: 2
}

for (const key in testData) {
    console.log(key);
}

console.log(Object.getOwnPropertyNames(testData))   // ['a','b']
  1. Object.defineProperty和Object.defineProperties

object包括属性和方法,其中属性分为数据属性和访问器属性

  • 数据属性

    • configurable:通过delete删除并重新定义属性,是否可修改属性的特性,已经是否可把它改为访问器属性。
    • enumerable:是否可通过for-in循环。
    • writable:属性值是否可修改。
    • value:属性实际的值,默认为undefined
  • 访问器属性

    • configurable
    • enumerable
    • get:获取函数,在读取属性时调用,默认undefined
    • set:设置函数,在写入属性时调用,默认undefined
  • 默认值:

    • configurable、enumerable、writable:false
    • get、set、value:undefined
    • 定义属性时,数据属性和访问器属性不能同时存在,报错

Object.defineProperty(obj, propertyName, descriptor) //用于对象的单个属性定义,参数:对象,属性名称,描述符对象

Object.defineProperties(obj, descriptor)//用于对象多个属性定义,参数:对象、描述符对象(属性与添加或修改的属性一一对应)

let test = {};

Object.defineProperty(test, 'name', {
    configurable: true,
    enumerable: true,
    writable: true,
    value: "Jian"
})

let t = 0;
Object.defineProperties(test, {
    age: {
        configurable: true,
        enumerable: true,
        get(){
            return t
        },
        set(newVal){
            t = newVal
        }
    },
    sex: {
        configurable: true,
        enumerable: true,
        //writable: true,
        value: "male"
    },

})
test.sex = 2
console.log('test.name', test.name)   //Jian
console.log('test.age', test.age)     //0
console.log('test.sex', test.sex)     //male, test.sex不生效

  1. Object.create
  • Object.create()方法创建一个新的对象,并以方法的第一个参数作为新对象的__proto__属性的值(根据已有的对象作为原型,创建新的对象。)
  • Object.create()方法还有第二个可选参数,是一个对象,对象的每个属性都会作为新对象的自身属性,对象的属性值以descriptor(Object.getOwnPropertyDescriptor(obj, 'key'))的形式出现,且enumerable默认为false
const person = {
    isHuman: false,
    printIntroduction: function () {
        console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
    }
};
const me = Object.create(person);

me.name = "lubai";
me.isHuman = true;
me.printIntroduction();

console.log(person);

const myObject = Object.create(null)

传入第二个参数是怎么操作的呢?

function Person(name, sex) {
    this.name = name;
    this.sex = sex;
}

const b = Object.create(Person.prototype, {
    name: {
        value: 'coco',
        writable: true,
        configurable: true,
        enumerable: true,
    },
    sex: {
        enumerable: true,
        get: function () {
            return 'hello sex'
        },
        set: function (val) {
            console.log('set value:' + val)
        }
    }
})

console.log(b.name)     // coco
console.log(b.sex)      // hello sex

那么Object.create(null)的意义是什么呢? 平时创建一个对象Object.create({}) 或者 直接声明一个{} 不就够了?

Object.create(null)创建一个对象,但这个对象的原型链为null,即Fn.prototype = null

const b = Object.create(null) // 返回纯{}对象,无prototype

b // {}
b.__proto__ // undefined
b.toString() // throw error

所以当你要创建一个非常干净的对象, 没有任何原型链上的属性, 那么就使用Object.create(null). for in 遍历的时候也不需要考虑原型链属性了.

  1. Object.assign 合并对象,接收一个目标对象和一个或多个源对象作为参数,将每个源对象中可枚举(Object.propertyIsEnumeralbe()返回true)和自有属性(Object.hasOwnProperty()返回true)复制到目标对象。
  • 以字符串和符合为键的属性会被复制。
  • 对每个符合条件的属性,使用源对象上的[[get]]取得属性的值,然后使用目标对象上的[[set]]设置属性的值
  • 浅拷贝,类似于{...a, ...b},不能在两个对象间转移获取函数和设置函数
let dest = {}
let src = {id: 'src', a: {}}
let result = Object.assign(dest, src)
console.log(dest === result) //true
console.log(result) //{id: 'src'}
console.log(dest.a === src.a) //true

dest = {
  set a(val){
    console.log(`Invoked dest setter with param ${val}`)
  }
}
src = {
  get a(){
    console.log('Invoked src getter')
    return 'foo'
  }
}
Object.assign(dest, src) 
//Invoked src getter
//Invoked dest setter with param foo
console.log(dest)
//{set a(val){}}
  1. Object.is
const a = {
    name: 1
};
const b = a;
console.log(Object.is(a, b))    // true

console.log(Object.is({}, {}))  // false
  1. Object.getOwnPropertyDescriptor和Object.getOwnPropertyDescriptors

返回指定对象的属性描述符对象

const obj1 = {}
Object.defineProperty(obj1, 'p', {
  value: 'good',
  writable: false
})
console.log(Object.getOwnPropertyDescriptor(obj1, 'p'))
console.log(Object.getOwnPropertyDescriptors(obj1))
/*
{
	value: 'good',
	writable: false,
	enumerable: false,
	configurable: false
}
{
	p: {
		value: 'good',
		writable: false,   //可写,修改属性值
		enumerable: false, //可被for in遍历
		configurable: false //删除属性或修改属性特性
	}
}
*/

8、数组

  1. Array.flat

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]

const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]

//使用 Infinity,可展开任意深度的嵌套数组(无限大)
const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  1. Array.includes includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。 Array.includes(valueToFind, fromIndex)
const array1 = [1, 2, 3];

console.log(array1.includes(2));

const pets = ['cat', 'dog', 'bat'];

console.log(pets.includes('cat'));

其实它有两个参数, 只不过我们平时只使用一个.

  • valueToFind 需要查找的元素值。

  • fromIndex 可选 从fromIndex 索引处开始查找 valueToFind。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。

[1, 2, 3].includes(2);     // true
[1, 2, 3].includes(4);     // false
[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true
[1, 2, NaN].includes(NaN); // true

// fromIndex 大于等于数组长度
var arr = ['a', 'b', 'c'];

arr.includes('c', 3);   // false
arr.includes('c', 100); // false

// 计算出的索引小于 0
var arr = ['a', 'b', 'c'];

arr.includes('a', -100); // true
arr.includes('b', -100); // true
arr.includes('c', -100); // true
  1. Array.find find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。 Array.find(callback(element, index, array(数组本身)))

callback 在数组每一项上执行的函数,接收 3 个参数:

  • element 当前遍历到的元素。
  • index可选 当前遍历到的索引。
  • array可选 数组本身。
const test = [
    {name: 'lubai', age: 11 },
    {name: 'xxx', age: 100 },
    {name: 'nnn', age: 50}
];

function findLubai(teacher) {
    return teacher.name === 'lubai';
}

console.log(test.find(findLubai));
  1. Array.from
  • 4.1 Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

    • arrayLike 想要转换成数组的伪数组对象或可迭代对象。
    • mapFn 可选 如果指定了该参数,新数组中的每个元素会执行该回调函数。
  • 4.2 Array.from() 可以通过以下方式来创建数组对象:

  • 伪数组对象(拥有一个 length 属性和若干索引属性的任意对象)
  • 可迭代对象(可以获取对象中的元素,如 Map和 Set 等)
console.log(Array.from('foo'));

console.log(Array.from([1, 2, 3], x => x + x));

const set = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(set);
// [ "foo", "bar", "baz" ]

const map = new Map([[1, 2], [2, 4], [4, 8]]);
Array.from(map);
// [[1, 2], [2, 4], [4, 8]]

const mapper = new Map([['1', 'a'], ['2', 'b']]);
Array.from(mapper.values());
// ['a', 'b'];

Array.from(mapper.keys());
// ['1', '2'];

所以数组去重我们可以怎么做?

function unique (arr) {
  return Array.from(new Set(arr))
  // return [...new Set(arr)]
}
const test = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a'];
console.log(unique(test));


function unique(arr) {
    const map = new Map();
    const array = []; // 数组用于返回结果
    for (let i = 0; i < arr.length; i++) {
        if (!map.has(arr[i])) { // 如果有该key值
            array.push(arr[i]);
            map.set(arr[i], true);
        }
    }
    return array;
}

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    const array = [];
    for (let i = 0; i < arr.length; i++) {
        if (!array.includes(arr[i])) { //includes 检测数组是否有某个值
            array.push(arr[i]);
        }
    }
    return array
}
  1. Array.of

Array.of() 方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

Array.of(7);       // [7]
Array.of(1, 2, 3); // [1, 2, 3]

那怎么去模拟实现它呢?

Array.of = function() {
    return Array.prototype.slice.call(arguments);
};

9、反射Reflect和代理Proxy

  • 反射Reflect

    • 将Object对象上的明显属于语言内部的方法,放在Reflect对象上
    • 让Object操作变为函数行为,eg:name in obj和delete,变为Reflect.has(obj,name)和Reflect.deleteProperty(obj,name)
    • Reflect对象的方法与Proxy对象的方法一一对应。让Proxy对象可以方便的调用对应的Reflect方法,完成默认行为。
    • 通过defineProperty设置writable为false的对象,不能使用Proxy
  • 代理Proxy

    • 定义: const proxy = new Proxy(target, handler),target代理对象,handle捕获器
    • handle参数get:trapTarget(目标对象),property(代理属性),receiver(代理对象)
    • handle参数set:trapTarget(目标对象),property(代理属性),value(赋给属性的值),receiver(代理对象)
const target = {
  foo: 'bar',
  baz: 'qux'
}
const handler = {
  get(trapTarget, property, receiver){
    let decoration = ''
    if(property === 'foo'){
      decoration = '!!!'
    }
    return Reflect.get(...arguments) + decoration
  },
  set(trapTarget, property, value, receiver){
    return Reflect.set(...arguments) + 'set'
  }
}

const proxy = new Proxy(target, handler)
proxy.foo = 'good'
console.log(proxy.foo) //good!!!
console.log(target.foo)//good

3、 面试题

1、如何生成Iterator对象?

function generateIterator(array) {
    let nextIndex = 0
    return {
        next: () => nextIndex < array.length ? {
            value: array[nextIndex++],
            done: false
        } : {
            value: undefined,
            done: true
        }
    };
}

const iterator = generateIterator([0, 1, 2])

console.log(iterator.next())    // {value: 0, done: false}
console.log(iterator.next())    // {value: 1, done: false}
console.log(iterator.next())    // {value: 2, done: false}
console.log(iterator.next())    // {value: undefine, done: true}

2、如何实现一个可以for of遍历的对象?

// 面试题: 实现一个可以for of遍历的对象
const obj = {
    count: 0,
    [Symbol.iterator]: () => {
        return {
            next: () => {
                obj.count++;
                if (obj.count <= 10) {
                    return {
                        value: obj.count,
                        done: false
                    }
                } else {
                    return {
                        value: undefined,
                        done: true
                    }
                }
            }
        }
    }
}

for (const item of obj) {
    console.log(item)
}
 

或者

const iterable = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator],
};

for (const item of iterable) {
    console.log(item);
}

3、如何手写实现一个函数模拟Object.keys?

function getObjectKeys(obj) {
    const result = [];
    for (const prop in obj) {
        if (obj.hasOwnProperty(prop)) {     //注意 当使用for in遍历对象时,一定要进行hasOwnProperty判断,否则会遍历原型链上的值
            result.push(prop);
        }
    }
    return result;
}

console.log(getObjectKeys({
    a: 1,
    b: 2
}))

4、如何手写实现一个函数模拟Object.values?

function getObjectValues(obj) {
    const result = [];
    for (const prop in obj) {
        if (obj.hasOwnProperty(prop)) {
            result.push(obj[prop]);
        }
    }

    return result;
}

console.log(getObjectValues({
    a: 1,
    b: 2
}))

5、如何手写实现一个函数模拟Object.entries?

function getObjectEntries(obj) {
    const result = [];
    for (const prop in obj) {
        if (obj.hasOwnProperty(prop)) {
            result.push([prop, obj[prop]]);
        }
    }

    return result;
}

console.log(getObjectEntries({
    a: 1,
    b: 2
}))

6、如何实现一个浅拷贝函数?

// 实现浅拷贝函数
function shallowClone(source) {
    const target = {};
    for (const i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }

    return target;
}

const a = {
    b: 1,
    c: {
        d: 111
    }
}

const b = shallowClone(a);


b.b = 2222;
b.c.d = 333;

console.log(b)  // { b: 2222, c: { d: 333 } }
console.log(a)  // { b: 1, c: { d: 333 } }

7、如何模拟实现Array.flat?

// 使用 reduce、concat 和递归展开无限多层嵌套的数组
const arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];

function flatDeep(arr, d = 1) {
    if (d > 0) {
        return arr.reduce((res, val) => {
            if (Array.isArray(val)) {
                res = res.concat(flatDeep(val, d - 1))
            } else {
                res = res.concat(val);
            }
            return res;
        }, [])
    } else {
        return arr.slice()
    }
};

console.log(flatDeep(arr1, Infinity))
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

如果不考虑深度, 咱们直接给他无限打平

function flatten(arr) {
    let res = [];
    let length = arr.length;
    for (let i = 0; i < length; i++) {
        if (Object.prototype.toString.call(arr[i]) === '[object Array]') {
            res = res.concat(flatten(arr[i]))
        } else {
            res.push(arr[i])
        }
    }
    return res
}

// 如果数组元素都是Number类型
function flatten(arr) {
	return arr.toString().split(',').map(item => +item)
}

function flatten(arr){
    while(arr.some(item=>Array.isArray(item))){
        arr = [].concat(...arr);
    }
    return arr;
}

8、 for in 与 for of 的区别?

for in: 遍历数组输出索引,遍历对象输出key;

    缺点: 1.不仅会遍历当前对象,还会遍历原型链上的属性;
    2.不适合遍历数组

for of: 仅遍历当前对象

forEach 不会被break中断

9、如何把arguments转换成真数组?

  1. [...arguments]
  2. Array.from(arguments)
  3. Array.prototype.slice.call()