你知道 ECMAScript 与 Javascript的关系吗?

565 阅读15分钟

ECMAScript 介绍

ECMAScript 也是一门脚本语言,通常简称为 ES, 通常我们会把他看作 javaScript 的标准化规范. 但是实际上 javaScript 是 ECMAScript 的扩展语言.

ECMAScript 只提供了最基本的语法, 通俗点来说就是约定了我们的语法该如何去写,例如我们该怎么样去定义变量和函数,或者是如何去实现循环判断之类的语句. 它只是停留在语言层面,并不能实现我们实际应用中的开发.

而 js 实现了 ECMAScript 的标准, 并且在此基础上做了一些扩展,使得 js 能够在 浏览器环境中实现dom和bom操作,在node环境下实现读取文件等操作

用图表示

js在 web环境表示 ECMAScript + web api image.png js在 node 环境 ECMAScript + node api

image.png

也就是说javascript 语言本身就是指 ECMAScript, 只是在不同环境有着相应的扩展

并且 ECMAScript 每年都在更新, js也就随之更新, 从 ES2015之后开始以发行年份命名 image.png

es2015概述

这里将介绍 es2015常用的一些特性, 需要注意的是我在这里只是简单介绍一些es2015内容, 不会很深入的探究, 可以作为面试复习, 经验丰富的人建议直接跳过,,,,

let

在此之前, ECMAScript 只有两种作用域, 分别是 全局作用域和函数作用域,es2015新增了块级作用域(一个if语句, 一个 for循环都是一个块),在之前, 不存在块的概念,例如

if (true) {
  var foo = 'zce'
}
console.log(foo) // // lhs

在块的外面可以获取到 foo,这对于一些比较复杂的代码是不利的并且很不安全. 但是 现在 用 let如下, 就会报一个未定义的错误.因为 if是一个块, 在块内部定义的变量,外部无法访问

if (true) {
  let foo = 'lhs'
}
console.log(foo) // ReferenceError: foo is not defined

一个典型的计数例子

for (var i = 0; i < 3; i++) {
  for (var i = 0; i < 3; i++) {
    console.log(i)
  }
  console.log('内层结束 i = ' + i)
}

for (let i = 0; i < 3; i++) {
  for (let i = 0; i < 3; i++) {
    console.log(i)
  }
  console.log('内层结束 i = ' + i)
}

第一个 双层for 只打印了一次 0 1 2, 第二个才打印了三次, 为什么呢? 我们首先来看var定义的for, 在外层已经定义了一个 var i, 内层定义就会被忽略掉, 当内存 for 执行完时 此时 i = 3, 那么外层因为 i = 3 也不能执行了,所以 执行了 一次内部循环.
第二个 let 循环外层定义了一个 let i, 内层也定义了一个 let i, 但是两者其实互不影响, 内层i = 3, 外层的 i还是 0,所以能够执行三次,当然在实际开发中不建议这么做, 不然你可能连老花眼都要看出来了. let 不能在声明前被使用

console.log(foo) // ReferenceError: Cannot access 'foo' before initialization
let foo = 1

console.log(name) // undefined
var name = 1

foo 报错就是在未定义之前使用, 这也就是所谓的暂时性死区, 用 var 声明的是未定义是因为js代码在运行前也会有一个短暂的编译阶段,在编译阶段会将 var 声明提前, var name; 在运行阶段才会对其赋值.

const

const就是常量的意思,它有个 let 没有的特性就是一旦声明赋值后便不能更改

const a = 1
a = 2 // TypeError: Assignment to constant variable.

但是 如果是一个对象, 我们可以为这个对象增加删除属性, 因为 const 保存的是对象的引用地址,只要地址不改变,是运行对对象进行操作的

const obj = {a:1}
obj.b = 2  // 正常不会有问题

obj = {} // 重新修改了对象的地址就会报错

还有 const 在声明时就必须定义, 这也符合 const 原则, 其他的 都与let 一样,这里就不重复说明.

数组解构

在数组解构没有出来之前,我们获取每个数组的值是这样的

const arr = [100, 200, 300]

const foo = arr[0]
const bar = arr[1]
const baz = arr[2]
console.log(foo, bar, baz)

解构出来后就是这样的,有没有感觉很 nice

const arr = [100, 200, 300]
const [foo, bar, baz] = arr
console.log(foo, bar, baz)

并且我们还可以这样,这样,这样

// 只需要结构最后一个值可以在前面加两个逗号
const [, , baz] = arr
console.log(baz)
//  rest 就是除了foo的数组
const [foo, ...rest] = arr
console.log(rest)
// more为 undefind
const [foo, bar, baz, more] = arr
console.log(more)
// 可以给 数组的值赋予默认值
const [foo, bar, baz = 123, more = 'default'] = arr
console.log(bar, more) // 200 default

对象解构

简单使用

const obj = { name: 'lhs', age: 18 }

const { name } = obj
console.log(name) // lhs

还是简单实用

const name = 'tom'
// 防止全局有个 name,可以通过这种方式重命名
const { name: objName } = obj
console.log(objName)

const name = 'tom'
// 还可以赋默认值
const { name: objName = 'jack' } = obj
console.log(objName)

实用小案例

const { log } = console
log('foo')
log('bar')
log('123')

可以把log结构出来打印, 想要更简单,可以给它重命名一个 a, 你学废了吗

模版字符串

简单使用

const str = `this is a string`

相比于 引号, 它可以换行继续写, 但是 引号是不行的, 而且它可以通过 ${} 插入表达式,表达式的执行结果将会输出到对应位置

const name = 'tom'
// 可以通过 ${} 插入表达式,表达式的执行结果将会输出到对应位置
const msg = `hey, ${name} --- ${1 + 1}`
console.log(msg) // hey, tom --- 2

模版字符串还有一个比较强大的功能, 带标签的 模版字符串

const str = console.log`hello world`   // [ 'hello world' ]

console.log是一个函数, 模版字符串写在它后面就代表 调用了该函数,并且返回了一个数组 我们来自己定义一个函数,并且用模版字符串调用

function Tag(strings) {
  return strings
}
const res = Tag`hello, how are you`
console.log(res)  // [ 'hello, how are you' ]

可以看到 Tag 函数可以接收一个参数, 在内部可以对这个参数做一些操作, 我们再看

function Tag (strings, name, gender) {
  // 能够接受多个参数, 第二个参数开始是模版字符串里面的值
  console.log(strings, name, gender)  // [ 'hey, ', ' is a ', '.' ] tom false
  const sex = gender ? 'man' : 'woman'
  return strings[0] + name + strings[1] + sex + strings[2]
}
//  如果模版字符串中有表达式, 那么 strings这个数组就会 被 表达式拆分为多个值
const result = Tag`hey, ${name} is a ${gender}.`

console.log(result) // [ 'hey, ', ' is a ', '.' ] tom false

这也就模版字符串作为函数调用的强大之处了, 它的作用是什么呢, 就是对一些字符串的处理能够更加方便,像上面传入一个 gender,函数内部来判断 返回什么值.

字符串的扩展方法

  1. includes()
  2. startWith()
  3. endWith() 三者都是判断字符串中是否有指定的字符串, 看方法名我们也可以清晰知道, startWith 用来判断字符串开头是否包含 某些字符, endsWith 同理判断末尾, includes 判读 整个字符串中是否有某些字符.
// 字符串的扩展方法

const message = 'you are a very handsome boy'

console.log(
  message.startsWith('you'), // true
  message.endsWith('boy'),  // true
  message.includes('very')  // true
)

相比于之前使用 indexof 或者 正则来判断是方便了许多的.

参数默认值

// 默认参数一定是在形参列表的最后
function foo (a, b, c = 3) {
 console.log(a,b,c)
}
// 第三个值不传 foo 会直接拿 c = 3, 如果传了就拿传入的值
foo(1, 2)

剩余参数

function foo () {
  console.log(arguments) // [Arguments] { '0': 1, '1': 2, '2': 3, '3': 4 }
}

// ...形式只能在最后并且只能使用一次
function foo1 (first, ...args) {
  console.log(args) // [2, 3, 4]
}

foo(1, 2, 3, 4)
foo1(1, 2, 3, 4)

在之前我们可以通过 arguments 伪数组获取 所有参数, 现在我们可以通过 ...args方式拿到所有参数, 也可以获取剩余参数, 在我看来这种方式更加便捷了.

展开数组参数

const arr = ['foo', 'bar', 'baz']
console.log(...arr)  // foo bar baz

这个看起来也很清晰,不多介绍

箭头函数

// function inc (number) {
//   return number + 1
// }

// 最简方式
// const inc = n => n + 1

箭头函数有个最大的特点就是不会改变 this 的指向

const person = {
  name: 'tom',
  say: function () {
    console.log(`hi, my name is ${this.name}`) // hi, my name is tom
  },
  say1: () => {
    console.log(`hi, my name is ${this.name}`) // hi, my name is undefined
  },

}

person.say()
person.say1()

this是在调用的时候确定指向, 我们看到两者区别, 箭头函数 this 代表的 是 person 的this , person 的this指向全局, 而 say 的 this 指向的是 person, person 上 有 name, 所以 指向 name.

对象字面量增强

在我看来就是进一步简化了 对象的书写

const bar = '345';
const obj = {
  foo: 123,
  // bar: bar
  // 属性名与变量名相同,可以省略 : bar
  bar,
  // method1: function () {
  //   console.log('method111')
  // }
  // 方法可以省略 : function
  method1 () {
    console.log('method111')
    // 这种方法就是普通的函数,同样影响 this 指向。
    console.log(this)
  },
  // Math.random(): 123 // 不允许
  // 通过 [] 让表达式的结果作为属性名
  [Math.random()]: 123
}

Object.assign

可以将 多个原对象中的属性复制到一个目标对象中, 需要注意的是 它会修改目标对象

const source1 = {
  a: 123,
  b: 123,
};

const source2 = {
  b: 789,
  d: 789,
};

const target = {
  a: 456,
  c: 456,
};
//                            目标对象  源对象1 源对象2
const result = Object.assign(target, source1, source2);

console.log(target); // { a: 123, c: 456, b: 789, d: 789 }
console.log(result === target);  // true

proxy

如果我们想要监听某个对象的读写, 可以使用 Object.defineProperty, Vue3.0以前就是通过这个属性实现的数据双向绑定.
es2015中提供了 proxy 专门用来为对象设置代理器的. 基本用法如下:

const person = {
  name: 'zce',
  age: 20,
};
const personProxy = new Proxy(person, {
  // get 监听属性的读取, 返回值将会作为外部访问这个属性得到的结果
  // target: 目标对象, p, 外部访问的所访问的属性名
  get(target, p) {
    console.log(target, p);
    // 判读 p属性是否存在于 target中, 存在则返回该值, 否则返回default
    return p in target ? target[p] : 'default';
  },
  // set监听属性的 设置
  set(target, p, value) {
    console.log(p, value);
    // 中间可以处理逻辑, 比如 value必须是数字,否则报错
    // Reflect 可以原封不动的将 set 功能直接 返回
    return Reflect.set(target, p, value);
  },
});
console.log(personProxy);
personProxy.aaa = '30';

相比于 defineProperty只能监听对象的读写操作, Proxy有更强大的功能,

  1. 比如监听 对象删除, 监听函数调用等等
  2. 不需要入侵源对象

Reflect

Reflect 是 es2015 中提供的全新的内置对象,它是一个静态类,不能通过 new Reflect() 方式创建实例. 只能通过 调用 静态类中的静态方法,比如 Reflect.get(), 其实 Reflect对象中实现的逻辑就是 Proxy处理对象的默认实现, 在 Reflect 总共有13个处理方法,在 proxy都有一一对应.

const person = {
  name: 'zce',
  age: 20,
};
const personProxy = new Proxy(person, {
  get(target, p) {
    console.log('实现监听逻辑');
    return Reflect.get(target, p)
  },
  // set监听属性的 设置
  set(target, p, value) {
    console.log('实现监听逻辑');
    return Reflect.set(target, p, value);
  },
});
console.log(personProxy);
personProxy.aaa = '30';

使用上我们可以 这样


const person = {
  name: 'zce',
  age: 20,
};
console.log(Reflect.has(person, 'name')); // true
console.log(Reflect.set(person, 'a', 1)); // true
console.log(person); // { name: 'zce', age: 20, a: 1 }

对于 Reflect更多api可以看 mdn 文档,这里只做简要使用.

Promise

一种更有的异步编程方案, 解决 回调嵌套过深的问题, 这个相信大家都会用 ,,, 我另外两篇文章有介绍promise源码解析,有兴趣可以了解一下.

class

于一些老牌编程语言比较类似, 可以创建构造函数, 在类里面定义属性和方法, 可以实例化这个类成一个对象, 可以有静态方法, 可以实现类的继承等等

class Person {
  constructor (name) {
    this.name = name
  }

  say () {
    console.log(`hi, my name is ${this.name}`)
  }
  static eat() {
      console.log('eat apple')
  }
}

const p = new Person('tom')
p.say()


class Student extends Person {
  constructor (name, number) {
    super(name) // 父类构造函数
    this.number = number
  }

  hello () {
    super.say() // 调用父类成员
    console.log(`my school number is ${this.number}`)
  }
}

Set 数据结构

与传统数组类似, 但是数组里面的值不能重复,添加数据可以使用 add

const set = new Set();
set.add(1).add(2).add(3);
console.log(set);

有增删改差, 遍历的api,与数组类似

image.png

Map

于 对象很类似, 本质都是键值对集合, 只不过 对象中的键只能是字符串类型, 如果将其他类型作为键 对象会默认将之转换为字符串, 这不利于构建复杂的数据结构.
Map 可以是任意类型的 键和值

const m = new Map();
const tom = { name: 'tom' };
m.set(tom, 90);
console.log(m); // Map(1) { { name: 'tom' } => 90 }
console.log(m.get(tom)); // 90

Symbol

两个 Symbol 永远不会相等

const s = Symbol();
console.log(s);
console.log(typeof s); // symbol

// 两个 Symbol 永远不会相等

console.log(Symbol() === Symbol()); // false

可以使用 Symbol 为对象添加用不重复的键

const obj = {};
obj[Symbol()] = '123';
obj[Symbol()] = '456';
console.log(obj); // { [Symbol()]: '123', [Symbol()]: '456' }

使用小案例,我们可以用 symol创建私有变量

const name = Symbol();
const person = {
  [name]: 'zce',
  say() {
    console.log(this[name]);
  },
};
// 只对外暴露 person

// b.js =======================================

// 由于无法创建出一样的 Symbol 值,
// 所以无法直接访问到 person 中的「私有」成员
// person[Symbol()]
person.say();

symbol为我们提供了 一个全局注册表,使得 两个 symbol.for()可以相等

const s1 = Symbol.for('foo');
const s2 = Symbol.for('foo');
console.log(s1 === s2);  // true

for...of

for ... of 以后将会作为遍历所有数据结构的统一方式,而其实只要明白for...of 内部原理,就可以遍历任意一个数据结构
基础使用:

const arr = [1, 2, 3, 4];
// 遍历数组
for (const item of arr) {
  console.log(item); // 1 2 3 4
}
// 遍历 Set 与遍历数组相同

const s = new Set([1, 2]);

for (const item of s) {
  console.log(item); // 1 2
}
// 遍历 Map 可以配合数组结构语法,直接获取键值

const m = new Map();
m.set('foo', '123');
m.set('bar', '345');

for (const [key, value] of m) {
  console.log(key, value); // foo 123    bar 345
}

// 普通对象不能被直接 for...of 遍历

const obj = { foo: 123, bar: 456 };

for (const item of obj) {
  console.log(item);  // TypeError: obj is not iterable
}

为什么 for ... of 不能遍历对象呢, 这与可迭代对象有关, 请看下一个内容

可迭代对象接口

实现 可迭代接口 就是 for ... of 的前提, 换句话说, 只要这个数据接口实现了 可迭代接口,就能够被 for of 遍历, 在浏览器控制台打印能够被 for of遍历的数据结构, 你会发现他们原型链上都一个相同的方法,如下图

image.png 接着我们在数组中调用这个方法看会有些什么

image.png 然后我们发现这个方法上返回值上又有一个 next 方法,再次调用, 你会发现如下

image.png 这个时候你可能会想到, 在这个迭代器当中维护了一个数据指针,每调用next指针就会往后移动一位, done的作用就表示数据是否遍历完了
小总结: 所有被可以被 for of遍历的对象都要必须实现一个叫 iterator 的接口, 也就是在内部必须挂载 iterator 方法, 这个方法需要返回带有 next 的对象, 我们不断调用 next 就可以实现对内部数据的遍历.

实现可迭代对象接口

上一小节我们知道了一些概念, 这里我们简单实现一个 可迭代对象接口,如下代码

let obj = {
  arr: [1, 2, 3],
  [Symbol.iterator]: function () {
    let index = 0;
    // 保存this指针, 使得能够正确获取到 arr
    let self = this;
    // 可迭代方法返回的 是一个对象,对象上有一个 next 方法
    return {
      next: function () {
        // next 返回一个 迭代结果, 当 done 为 false 说明数据没有被遍历完
        return {
          // 每次获取新的值
          value: self.arr[index],
          // index 需要 ++, 不然就是死循环
          done: index++ >= self.arr.length,
        };
      },
    };
  },
};
for (const item of obj) {
  console.log(item); // 1 2 3
}

在 obj 对象中 我们建了一个 Symbol.iterator 接口的方法, 该方法返回 一个对象, 该对象有个next方法, 调用 next 可以获取下一个数据的值, 这里我们给对象中加了一个 arr:[1,2,3], 内部通过 next遍历. 然后 在 外面 for of 发现就可以遍历拿到 1 2 3.

生成器 Generator

主要是为了避免编程中回调过深, 提供一个更好的异步解决方案, 首先我们看一下基本语法

function * foo () {
  console.log('1111')
  yield 100
  console.log('2222')
  yield 200
  console.log('3333')
  yield 300
}

const generator = foo()

console.log(generator.next()) // 第一次调用,函数体开始执行,遇到第一个 yield 暂停
console.log(generator.next()) // 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停
console.log(generator.next()) // 。。。
console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined

// 执行顺序
// 1111
// { value: 100, done: false }  第一次调用 next
// 2222
// { value: 200, done: false }  // 第二次调用 next
// 3333
// { value: 300, done: false } // 第三次
// { value: undefined, done: true } // 第四次

在普通函数后面加个 * 就是一个生成器函数, 生成器函数中 可以有 yield 关键字, 作用在后面介绍 调用这个函数会返回一个 生成器对象, 并且代码执行到第一个yield前面就会停止, 知道执行 该对象的 next方法, 就会执行下一个 yield 之前的代码. yield 后面的值 会作为 返回对象的 value值.

es2016

这个版本只有小更新,

  1. 新增了一个 includes方法, 前面有介绍
  2. 新增了一个指数运算符
// 之前需要这样写
console.log(Math.pow(2, 10))
// 现在可以这样写
console.log(2 ** 10)

es2017

为对象新增了三个方法

  1. Object.values(), 它以数组形式返回对象中所有的值
const obj = {
  foo: 'value1',
  bar: 'value2',
};
console.log(Object.values(obj)); // [ 'value1', 'value2' ]
  1. Object.entries, 以数组形式返回对象中所有的键和值, 这就可以将对象转数组后使用forof遍历对象了
const obj = {
  foo: 'value1',
  bar: 'value2',
};
console.log(Object.entries(obj)); // [ [ 'foo', 'value1' ], [ 'bar', 'value2' ] ]
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}
  1. Object.getOwnPropertyDescriptors, 为对象中的 get 和 set 服务
const p1 = {
  firstName: 'l',
  lastName: 'hs',
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  },
};
// 调用 p1.funnName会调用get方法
console.log(p1.fullName); // l hs
// 通过 assign 复制这个对象并改变firstName
const p2 = Object.assign({}, p1);
p2.firstName = 'w';
// 会发现还是答应 l hs
console.log(p2); // l hs

首先我们看到上面 get 方法在 firstName改变的情况下 get 的返回值没有变, 因为 它将 fullName这个方法当作了一个普通属性, es2017就提供了一个方法解决这个问题

const p1 = {
  firstName: 'l',
  lastName: 'hs',
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  },
};

// 首先获取 p1 对象的完整描述信息
const descriptors = Object.getOwnPropertyDescriptors(p1);
// console.log(descriptors)
// 通过 definProperties 复制 get 和 set
const p2 = Object.defineProperties({}, descriptors);
// 就能够修改 get 和 set
p2.firstName = 'w';
console.log(p2.fullName); // w hs

这个时候改变 firstName, get 方法也会随之改变
es2017还新增了两个字符串操作方法 padStart, padEnd, 他们就是用给定的字符串填充目标字符串的开头或者结束位置, 适合一些对齐或者补0操作

let str = '12.0';
console.log(str.padStart(5, '0')); // 012.0
console.log(str.padEnd(5, '0')); // 12.00

总结

至此 对于 ECMAScript 的内容简单过了一遍.想看最新的 ECMAScript 版本内容,可以去官网查看. 文中可能有许多错别字, 体谅体谅🥺.