ES6理解(持续更新中~)

564 阅读8分钟

简介

  • ES6:指2015年6月发布的ES2015标准, 但是很多人在谈及ES6的时候, 都会把ES2016 ES2017等标准的内容也带进去。严格来说,用年份更好一些. 但是也无所谓, 纠结这个没多大意义。
  • ESNext:一个泛指, 它永远指向下一个版本。比如当前最新版本是ES2020, 那么ESNext指的就是2021年6月将要发布的标准。

let 和 const

  • 变量提升问题
console.log(i) // undefined
var i = 1;

console.log(letI) // 报错
let letI = 2;
  • 作用域问题
for (var i = 0; i <= 3; i++) {
    setTimeout(function () {
        console.log(i); // 4 4 4 4
    }, 10);
}

// ES5 中其他方法实现打印正确结果
for(var i = 0; i <=3; i++) {
    (function (i) {
        setTimeout(function () {
            console.log(i); // 0 1 2 3
        }, 10);
    })(i);
}

原因:

  1. var定义的变量是全局的, 所以全局只有一个变量 i
  2. setTimeout是异步, 在下一轮事件循环, 等到执行的时候, 去找i变量的引用。所以函数找到了遍历完后的 i, 此时它已经变成了4
for(let i = 0; i <= 3; i++){ 
    setTimeout(function() {  
        console.log(i); // 0 1 2 3
    }, 10);
} 

原因:

  1. let引入了块级作用域的概念, 创建setTimeout函数时,变量 i 在作用域内。对于循环的每个迭代,引用的 i 是 i 的不同实例。
  • let 定义变量,const 定义常量(不可修改)

箭头函数

  • 简写箭头函数
const arrowFn = (value) => Number(value);
// 相当于
const arrowFn = function(value) {
    return value
}
// 注:如果想 return 一个 {} 的话
const arrowFn = () =>({}); // {}         √
const arrowFn = () =>{}; // undefined    ×
// 相当于
const arrowFn = () => {
    return undefined
}
  • 箭头函数不能被用作构造函数
  1. 构造函数会干嘛? 改变this指向到新实例出来的对象
  2. 箭头函数会干嘛?this指向是定义的时候决定的
  • 最大的区别:箭头函数里的this是定义的时候决定的, 普通函数里的this是使用的时候决定的
const teacher = {
    name: 'fj',
    getName: function () {
        return `${this.name}`
    }
}
console.log(teacher.getName()); // fj

const teacher = {
    name: 'fj',
    getName: () => {
        return `${this.name}`
    }
}
console.log(teacher.getName()); // undefined 因为此时 this 指向 window
// 注:在 js 中运行打印 undefined,在 html 中运行打印 空

class

class Test {
    _name = '';
    constructor(name) {
        this.name = name; // 在实例化的同时做了set操作
    }
    // 静态属性:只能通过 class 调用
    static getFormatName() {
        return `${this.name} - xixi`;
    }
    // 实例属性:get、set (只能通过 new 出的实例调用)
    // 属性名需要一致,所以可用另一个变量存其真实的值
    get name() {
        return this._name;
    }

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

console.log(new Test('fj').name); // name setter    fj
console.log(Test.getFormatName()); // Test - xixi

// ES5 中
1. 实例属性:Test.prototype.xxx
2. 静态属性:Test.xxx
  • get、set作用:进行数据劫持,对原本数据进行封装

模板字符串

const name = 'fj';
`${name}haha` ==  name + 'haha'

// 对换行比较友好,直接换行
const huanh = `fj
fj
fj`
// 相当于
const huanh = 'fj\nfj\nfj\n'; // 添加转义字符才可以

实现简单render功能

题目:编写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)(context); 
console.log(str) // 2021-10-01

高阶函数:运行一个函数会返回一个新的函数 render(template)(context)

function render(template) {
    return function(context){
        return template.replace(/\$\{(.*?)\}/g, (match, key) => context[key]);
    }
}
// 解析:
1. 通过正则匹配得到key值为: year month day
2. context 实际为  { year: '2021', month: '10', day: '01' },所以 context['year'] = 2021...
3. 所以context[key]即可得到结果

解构

  • 数组的解构
// 基础类型解构
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
  • 对象的结构
// 对象属性解构
// 可以不按照顺序,这是数组解构和对象解构的区别之一
// 因为对象左右两边属性名必须一致,否则会报错
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'}
console.log(f1, rename) // current1, test2

解构原理

  • 针对可迭代对象的Iterator接口,通过遍历器按顺序获取对应的值进行赋值
  • Iterator 是什么?
  1. Iterator 是一种接口,为各种不一样的数据结构提供统一的访问机制。
    任何数据结构只要有Iterator接口,就能通过遍历操作。
    ES6中的for-of的语法相当于遍历器,会在遍历数据结构时,自动寻找Iterator接口(对象不可用 for-of)
  • Iterator有什么用?
  1. 为各种数据结构提供统一的访问接口
  2. 使得数据结构能按次序排列处理
  3. 可以进行 for of进行遍历
  • 如何生成一个 可迭代对象?
function generateIterator(array) {
    let nextIndex = 0
    return {
        // 规定返回一个 next 函数,且函数里面有 value 和 done 两个属性
        next: () => nextIndex < array.length ? {
            value: array[nextIndex++],
            done: false
        } : {
            value: undefined,
            done: true
        }
    };
}
const iterator = generateIterator([0, 1, 2])
console.log(iterator.next())
  • 可迭代对象是什么?
  1. 是 Iterator 接口的实现,仅仅是协议,任何遵循该协议的对象都能成为可迭代对象。
  • 可迭代对象得有两个协议:
  1. 可迭代协议:对象必须实现 Iterator 方法。即对象或其原型链上必须有一个Symbol.Iterator: () => 迭代器协议
  2. 迭代器协议:必须实现一个next()方法,该方法返回对象有done(boolean)和value属性。
  • 实现 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)
}

遍历

  • for in
  1. 遍历数组时,key为数组下标字符串
  2. 遍历对象,key为对象字段名。
    缺点:
  3. for in 不仅会遍历当前对象,还包括原型链上的可枚举属性()
  4. for in 不适合遍历数组,主要应用为对象
  • for of 优点:
  1. 有着同 for in 一样的简洁语法,但是没有 for in 那些缺点
  2. 不同于 forEach 方法,它可以与break、continue 和 return 配合使用
  3. 提供了遍历所有数据结构的统一操作接口

Object

Object.keys

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

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

Object.values

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

const obj = { a: 1, b: 2 };
const values = Object.values(obj); // [1, 2]

Object.entries

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

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

手写实现函数模拟Object.keys、Object.values、Object.entries?

function getObjectFun(obj) {
    const result = [];
    for (const prop in obj) {
        if (obj.hasOwnProperty(prop)) {
            // Object.keys
            result.push(prop)
            // Object.values
            result.push(obj[prop]);
            // Object.entries
            result.push([prop,obj[prop]])
        }
    }

    return result;
}
console.log(getObjectFun{a: 1,b: 2})

Object.getOwnPropertyNames

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

Object.prototype.aa = '1111';

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

for (const key in testData) {
    console.log(key); // a b aa
}

console.log(Object.getOwnPropertyNames(testData)); // [ 'a', 'b' ]

Object.getOwnPropertyDescriptor

什么是descriptor? 对对应的属性描述符, 是一个对象. 包含以下属性:

  1. configurable 是否可修改配置。默认为true,让其属性可有特性(如果为false则下面属性均无效)
  2. writable 是否可写。默认false,任何对该属性的改写操作无效(严格模式下会报错)
  3. enumerable 是否可枚举。是否能在for-in中遍历或在Object.keys中列举出来

Object.defineProperty

直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

// writable 案例
const object1 = {};
Object.defineProperty(object1, 'p1', {
  value: 'fj',
  writable: true
});
object1.p1 = 'not fj';
console.log(object1.p1); // not fj,如果 writeable 为 false时,则为 fj

数据劫持 get set

const obj = {};
let val = undefined;
Object.defineProperty(obj,'name',{
    set: function(value) {
        console.log(`${value} - xxx`);
        val = value;
    },
    get: function() {
        return val;
    }
})
obj.name = 'fj';
console.log(obj.name)
// fj - xxx
// fj
const obj = new Proxy({},{
    get: function(target,propKey) {
        console.log(`getting ${propKey}`)
        return target[propKey];
    },
    set: function(target,propKey,value) {
        console.log(`setting ${propKey} ${value}`);
        return Reflect.set(target,propKey,value);
    }
})
obj.name = 'fj';
console.log(obj.name);
// setting name fj
// getting name
// fj

Reflect

  1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到 Reflect 对象上。
    现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。
    也就是说,从Reflect对象上可以拿到语言内部的方法
  2. 让Object操作都变成函数行为。
    某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  3. Reflect对象的方法与Proxy对象的方法一一对应,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
    这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。 总结:Reflect 是 js 语言想要慢慢优化自己之前不规范行为而引入的方法(可用可不用)

Object.assign

浅拷贝。相同属性时后面属性覆盖前面属性

let obj = {name: 'fj',age: 22,arr: [1,2]};
let newObj = Object.assign({},obj);
newObj.name = 'gx';
newObj.arr.push(3); // 引用类型时就出问题了
console.log(obj) // { name: 'fj', age: 22, arr: [ 1, 2, 3 ] }
console.log(newObj) // { name: 'gx', age: 22, arr: [ 1, 2, 3 ] }

手写浅拷贝

function shallowClone(source) {
    const target = {};
    for(let i in source) {
        if(source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }
    return target;
}
let obj1 = {a:1,b:2,c: {c1: 3}};
let obj2 = shallowClone(obj1);

Object.is

判断两个值是否为同一个值。

const a = {
    name: 1
};
const b = a;
console.log(Object.is(a, b)); // true
console.log(Object.is({}, {})); // false

Promise

Promise.all

function PromiseAll(promiseArray) {
    return new Promise(function (resolve, reject) {
        //判断参数类型
        if (!Array.isArray(promiseArray)) {
            return reject(new TypeError('arguments muse be an array'))
        }
        let counter = 0;
        let promiseNum = promiseArray.length;
        let resolvedArray = [];
        for (let i = 0; i < promiseNum; i++) {
            // 3. 这里为什么要用Promise.resolve?
            Promise.resolve(promiseArray[i]).then((value) => {
                counter++;
                resolvedArray[i] = value; // 2. 这里直接Push, 而不是用索引赋值, 有问题吗
                if (counter == promiseNum) { // 1. 这里如果不计算counter++, 直接判断resolvedArr.length === promiseNum, 会有问题吗?
                    // 4. 如果不在.then里面, 而在外层判断, 可以吗?
                    resolve(resolvedArray)
                }
            }).catch(e => reject(e));
        }
    })
}

Promise.allSeettled

function PromiseAllSettled(promiseArray) {
    return new Promise(function (resolve, reject) {
        //判断参数类型
        if (!Array.isArray(promiseArray)) {
            return reject(new TypeError('arguments muse be an array'))
        }
        let counter = 0;
        const promiseNum = promiseArray.length;
        const resolvedArray = [];
        for (let i = 0; i < promiseNum; i++) {
            Promise.resolve(promiseArray[i])
                .then((value) => {
                    resolvedArray[i] = {
                        status: 'fulfilled',
                        value
                    };

                })
                .catch(reason => {
                    resolvedArray[i] = {
                        status: 'rejected',
                        reason
                    };
                })
                .finally(() => {
                    counter++;
                    if (counter == promiseNum) {
                        resolve(resolvedArray)
                    }
                })
        }
    })
}

数组

Array.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]

手写函数扁平化

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))

Array.includes

判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

const array1 = [1, 2, 3];
console.log(array1.includes(2)); // true
console.log(array1.includes(4)); // false

Array.from

从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例

Array.from('foo'); // [ 'f', 'o', 'o' ]
Array.from([1, 2, 3], x => x + x; // [ 2, 4, 6 ]
Array.from(new Set([1,2,3,3])); // [ 1, 2, 3 ]

如何把arguments 转换为真数组

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

如何去重

function unique (arr) {
  return Array.from(new Set(arr))
  // return [...new Set(arr)]
}
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
}