Web前端基础知识:ES6篇

186 阅读16分钟

先导

个人在学习过程中,涉及和遇见的一些基础知识,对其进行了简单归纳总结,浅尝辄止,略显杂而不精,做个人参考用

注:内容基本都是摘抄自博客、网络或MDN等

ES6篇

1、var、let、const三者区别:

  • 变量提升
  • 暂时性死区
  • 块级作用域
  • 重复声明
  • 修改声明的变量
  • 使用

var:

声明的是全局变量,也是顶层变量

在函数中:使用var声明变量的时候,该变量是局部的,而如果不使用var,则该变量是全局的

注意:顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象

let:

let也会有提升,但是只是创建层面的提升,没有初始化,这样在使用时才会报错,形成暂时性死区

  • let 的「创建」过程被提升了,但是初始化没有提升。
  • var 的「创建」和「初始化」都被提升了。
  • function 的「创建」「初始化」和「赋值」都被提升了。

const:

首先const声明的变量不可改变,其次一旦声明必须初始化,const没有赋值

const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动


2、Generator

执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态

形式上,Generator 函数是一个普通函数,但是有两个特征:

  • function关键字与函数名之间有一个星号
  • 函数体内部使用yield表达式,定义不同的内部状态
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';	// yield关键字可以暂停generator函数返回的遍历器对象的状态
  return 'ending';
}

Generator 函数会返回一个遍历器对象,即具有Symbol.iterator属性,并且返回给自己

运行逻辑:

  • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

    返回对象:{value: x, done: true/false}

  • 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式

  • 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

  • 如果该函数没有return语句,则返回的对象的value属性值为undefined

  • 通过调用next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值

可以直接用for ... of循环迭代generator对象

优点

  • 因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。
  • generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。

3、Proxy/Reflect

1)Proxy

定义: 用于定义基本操作的自定义行为

本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)

元编程(Metaprogramming, 又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作

理解: 创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

使用:

var proxy = new Proxy ( target , handler )

  • target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))

  • handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

hander解析

常用拦截属性有:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • has(target,propKey):拦截propKey in proxy的操作,返回一个布尔值
  • deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
  • ownKeys(target):拦截Object.keys(proxy)、for...in等循环,返回一个数组
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作

2)Reflect

使用Reflect调用对象的默认行为(类似于其他语言的反射)

特点

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
  • 修改某些Object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false)
  • Object操作都变成函数行为

4、Decorator装饰器

Decorator,即装饰器,从名字上很容易让我们联想到装饰者模式

简单来讲,装饰者模式就是一种在不改变原类和使用继承的情况下,动态地扩展对象功能的设计理论。

ES6Decorator功能亦如此,其本质也不是什么高大上的结构,就是一个普通的函数,用于扩展类属性和类方法

用法 Docorator修饰对象为下面两种:

  • 类的装饰
  • 类属性的装饰

java基本一模一样

优点

  • 代码可读性变强了,装饰器命名相当于一个注释
  • 在不改变原有代码情况下,对原来功能进行扩展

5、Promise和async/await

1)Promise

(1)基础

  1. promise创建后会立即执行,resolve是用来变更promise的状态为fulfilled,只有当状态改变后,then中的函数才会被推入微任务。

  2. 在其中调用resolve(),仅相当于一个函数调用,改变状态,后面的语句会继续执行

  3. promise后面的then是一起注册的,所以要注意执行顺序

  4. 如果某一个then里面出现:return语句,那下一个then要等这个return执行之后的结果

关于执行顺序:

事件机制是 “先注册先执行”,下一个then的注册需要上一个then的同步代码执行完成,这里所说的 then 的注册,是指微任务队列的注册,并不是 .then 的方法的执行,没有注册的会等待

  • 链式调用的注册是前后依赖的:比如 new promise().then.then

  • 变量定义的方式,注册都是同步的:比如 p = new Promise(), p.then() 他们都是同步执行的。

(2)Promise API

Promise.all

接收一个promise数组参数,返回时的数组元素顺序和参数一致

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

示例:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// 将每个 url 映射(map)到 fetch 的 promise 中
let requests = urls.map(url => fetch(url));

// Promise.all 等待所有任务都 resolved
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

Promise.allSettled

Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组具有:

  • {status:"fulfilled", value:result} 对于成功的响应,
  • {status:"rejected", reason:error} 对于 error。
let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

/*
结果results
[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]
*/

Promise.race

同样接收一个promise数组, 但是返回最快响应的那个,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

Promise.any

Promise.any 只等待第一个 fulfilled promise,并将这个 fulfilled promise 返回。如果给出的 promise rejected,那么则返回 rejected promise AggregateError 错误类型的 error 实例—— 一个特殊的 error 对象,在其 errors 属性中存储着所有 promise error。

2)async/ await

async/await 在底层转换成了 promise then 回调函数。可以理解为是 promise + generator的语法糖。

async 函数调用不会造成阻塞,它会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象异步执行。

await暂停当前async的执行,然后把剩下的 async函数中的操作放到 then回调函数中。等待这个 Promise 完成,将其 resolve 的结果返回出来。

await会阻塞后面的任务,指的是下一行代码,await同行代码是会立即执行的

(1)async:

async 标识的函数,会返回promise 对象,

  • 如果async关键字函数返回的不是promise,会自动用Promise.resolve() 包装
  • 如果async关键字函数显式地返回promise,那就以你返回的promise为准

例如:

async function fn1(){
    console.log(123)
}

console.log(fn1())

// 打印如下
Promise {
  [[Prototype]]: Promise
  [[PromiseState]]: "fulfilled"
  [[PromiseResult]]: undefined
}

可以看见两点:

  1. 状态是fulfilled,说明自动resolve
  2. 无返回值,结果是undefined

(2)await

await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前

语法:[return_value] = await expression;

表达式(expression):一个 Promise 对象或者任何要等待的值。

返回值(return_value):返回 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。

:await这个语句是从右向左执行的,即先执行expression,然后遇见await,跳出返回,先执行async外的同步代码

参考阮一峰的理解:async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

参考题:

async function async1() {
    console.log('async1 start');
    await async2();                
    console.log('async1 end');
}

async function async2() {
    console.log('async2');	
    // 此时返回的Promise会被放入到回调队列中等待,await会让出线程(js是单线程),
    // 接下来就会跳出 async1函数 继续往下执行。
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();	// 同理,放到回调队列
}).then(function() {
    console.log('promise2');
});

console.log('script end');



/**
输出:
> script start
> async1 start
> async2
> promise1
> script end
> async1 end
> promise2
> undefined
> setTimeout
*/

6、Class

ES6 的类,完全可以看作构造函数的另一种写法,所以类的数据类型就是函数,而类本身就指向构造函数,

类的底层实现,还是依赖于原型链,事实上,类的所有方法都定义在类的prototype属性上面

1)Class基础

  1. 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

  2. 静态方法staticClass本身的方法,静态方法中的this指类,而不是示例,通过类直接调用

  3. 静态属性:Class本身的属性

  4. 私有方法和私有属性实现:

    私有方法

    • 命名上进行区别(私有的前面加下划线_prop
    • 将私有方法移出模块
    • 利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol

    私有属性

    • 在属性名之前,使用#表示(提案)
  5. 在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'

2)Class继承

ES6 的继承机制实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

  1. extendsclass ColorPoint extends Point{}

  2. super方法:表示父类的构造函数,用来新建父类的this对象。 子类必须在constructor方法中调用super方法,因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。

  3. Object.getPrototypeOf方法可以用来从子类上获取父类。

  4. super对象:super作为对象时,在普通方法中,指向父类的原型对象在静态方法中,指向父类

  5. 类的 prototype 属性和__proto__属性

    Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。 (3)子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型

实例:

class A {
}
class B {
}

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

const b = new B();

作为一个对象,子类(B)的原型(__proto__属性)是父类(A);

作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

个人理解是:子类的原型是父类,相当于父类构造函数的prototype对象


7、WeakMap、WeakSet

1)WeakMap

JavaScript 引擎在值“可达”和可能被使用时会将其保持在内存中。

所以如果我们使用对象作为常规 Map的键,那么当 Map存在时,该对象也将存在。它会占用内存,并且应该不会被(垃圾回收机制)回收

WeakMap在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object)的回收。

  1. WeakMapMap 的第一个不同点就是,WeakMap 的键必须是对象,不能是原始值
  2. WeakMap 只有以下的方法:
  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

因为如果一个对象丢失了其它所有引用,那么它就会被垃圾回收机制自动回收。但是在从技术的角度并不能准确知道 何时会被回收

这些都是由 JavaScript 引擎决定的。因此,从技术上讲,WeakMap的当前元素的数量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能没清理,也可能清理了一部分。因此,暂不支持访问 WeakMap的所有键/值的方法。

使用场景:

  1. WeakMap的主要应用场景是 额外数据的存储

    假如我们正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡 —— 这时候 WeakMap 正是我们所需要的利器。

  2. 缓存

    我们可以存储(“缓存”)函数的结果,以便将来对同一个对象的调用可以重用这个结果。

    如果使用Map,缺点是,当我们不再需要这个对象的时候需要清理 cache。

    如果我们用 WeakMap 替代 Map,便不会存在这个问题。当对象被垃圾回收时,对应缓存的结果也会被自动从内存中清除。

2)WeakSet

WeakSet 的表现类似:

  • Set类似,但是我们只能向 WeakSet 添加对象(而不能是原始值)。
  • 对象只有在其它某个(些)地方能被访问的时候,才能留在 set 中。
  • Set一样,WeakSet 支持 addhas delete 方法,但不支持 size keys(),并且不可迭代

它也可以作为额外的存储空间。但并非针对任意数据,而是针对“是/否”的事实。WeakSet 的元素可能代表着有关该对象的某些信息。

例如,我们可以将用户添加到 WeakSet中,以追踪访问过我们网站的用户

WeakMap WeakSet 最明显的局限性就是不能迭代,并且无法获取所有当前内容。那样可能会造成不便,但是并不会阻止WeakMap/WeakSet 完成其主要工作 ——成为在其它地方管理/存储“额外”的对象数据

3)总结

WeakMap 是类似于 Map 的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问它们,便会将它们与其关联值一同删除。

WeakSet 是类似于 Set 的集合,它仅存储对象,并且一旦通过其他方式无法访问它们,便会将其删除。

它们的主要优点是它们对对象是弱引用,所以被它们引用的对象很容易地被垃圾收集器移除。

这是以不支持 clearsizekeysvalues 等作为代价换来的……

WeakMapWeakSet被用作“主要”对象存储之外的“辅助”数据结构。一旦将对象从主存储器中删除,如果该对象仅被用作 WeakMapWeakSet的键,那么它将被自动清除。


8、如河判断是否为可迭代对象?

要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:

[Symbol.iterator]:一个无参数的函数,其返回值为一个符合迭代器协议的对象。

typeof obj[Symbol.iterator] === 'function';

9、箭头函数

普通函数箭头函数区别

  • 箭头函数中的this指向在定义时继承自外层第一个普通函数的this。且不会改变
  • 箭头函数没有原型,原型是undefined,所以本身没有this
  • call apply bind改变不了箭头函数指向
  • 箭头函数不能作为构造函数使用
  • 箭头函数不能用作Generator函数,不能使用yield关键字
  • 箭头函数不绑定arguments,取而代之用rest参数…

10、柯里化

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。

柯里化不会调用函数。它只是对函数进行转换

高级柯里化实现:

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {	// (1)
      return func.apply(this, args);
    } else {
      return function(...args2) { // (2)
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

当我们运行它时,这里有两个 if 执行分支:

  1. 如果传入的 args 长度与原始函数所定义的(func.length)相同或者更长,那么只需要使用 func.apply 将调用传递给它即可。
  2. 否则,获取一个偏函数:我们目前还没调用 func。取而代之的是,返回另一个包装器 pass,它将重新应用 curried,将之前传入的参数与新的参数一起传入。

然后,如果我们再次调用它,我们将得到一个新的偏函数(如果没有足够的参数),或者最终的结果。