ECMAScript与JavaScript

ECMAScript只提供了最基本的语法,JavaScript相当于ECMAScript的拓展语言。仅靠ES没有办法完成实际应用中的功能开发。JavaScript实现了ES标准,并在浏览器中可以操作BOM和DOM。

  1. Web中JS = ES + Web APIs(DOM, BOM)
  2. Nodejs中JS =ES + Node APIs(fs, net, etc. )

JavaScript的实现包括以下3个部分:

ECMAScript(核心)

描述了JS的语法和基本对象。

文档对象模型 (DOM)

处理网页内容的方法和接口

浏览器对象模型(BOM)

与浏览器交互的方法和接口

javascript 有三部分构成,ECMAScript,DOM和BOM,根据宿主(浏览器)的不同,具体的表现形式也不尽相同,ie和其他的浏览器风格迥异,IE 扩展了 BOM,加入了 ActiveXObject 类,可以通过 JavaScript 实例化 ActiveX 对象

  1. DOM 是 W3C 的标准; [所有浏览器公共遵守的标准]
  2. BOM 是 各个浏览器厂商根据 DOM在各自浏览器上的实现;[表现为不同浏览器定义有差别,实现方式不同]
  3. window 是 BOM 对象,而非 js 对象;javacsript是通过访问BOM(Browser Object Model)对象来访问、控制、修改客户端(浏览器)

ECMAScript的发展过程

ES版本每年迭代一个,ES6一般泛指ES2015之后的ES版本,包括ES2015-2020。

改进主要就是:

  1. 改进原有语法的不足(暂时性死区,块级作用域)
  2. 对原有语法增强(结构,展开,参数默认值和模板字符串)
  3. 全新的对象,方法,功能(Promise,Proxy,Object assign)
  4. 全新的数据类型和数据结构(Symbol,Set)

ECMAScript 2015的新特性

let与块级作用域

ES2015之前只有全局作用域和函数作用域,ES2015引入了块级作用域。比如if,for语句产生的花括号,之前块没有单独的作用域,外部也可以访问到,块级作用域的话外面就不能访问到这些了。

let取消了变量提升,解决了var暂时性死区的问题。已经声明的let不能重复声明。

// let 声明的成员只会在所声明的块中生效 -------------------------------------------
if (true) {
  // var foo = 'zce'
  let foo = 'zce'
  console.log(foo)
}
// let 修复了变量声明提升现象 --------------------------------------------
console.log(foo)
var foo = 'zce'

console.log(foo)
let foo = 'zce'

ES2015共有三种作用域

  • 全局作用域
  • 函数作用域
  • 块级作用域(新增)

const

在let的基础上增加了只读。最开始需要赋一个初始值,然后const声明的变量不允许指向新的内存地址(重新赋值是不可以的),但可以给指向的地址添加新的成员。比如给对象添加方法。

const name = 'zce'
//恒量声明过后不允许重新赋值
name = 'jack' //报错

//恒量要求声明同时赋值
const name
name = 'zce'//报错

//恒量只是要求内层指向不允许被修改
const obj = {}
//对于数据成员的修改是没有问题的
obj.name = 'zce'//可以执行

obj = {} //报错
  • 恒量声明过后不允许重新赋值
  • 恒量要求声明同时赋值
  • 恒量只是要求内层指向不允许被修改,对于数据成员的修改是没有问题的
  • 使用频率: const >let > var。因为const可以知道哪个变量不应该修改却被修改了,建议主要是用const声明变量。
    变量声明:let const
  • let const都是块级作用域,let是变量,const是常量

数组解构

  • 更方便的获取数组的成员
// 原来
const arr = [1, 2, 3];
// 用索引获取数组的元素
const foo3 = arr[2]; // 3

// 现在
// 数组解构并提取指定位置,需要保留前面的,
const [, , foo3] = arr;// 3

对象解构

  • 解构重命名也可以节约代码 提升阅读的语义
  • 对象中的变量名称会和作用域中原有的变量名称冲突,解构可以避免命名冲突
const obj = {name: 'kobe', age: 22}
const {name} = obj
console.log(name) // kobe

const name = 'jal'
const {name} = obj
console.log(name) // SyntaxError: Identifier 'name' has already been declared

const name = 'jal'
const {name: objName, sex = 'boy'} = obj
console.log(objName, sex) // yibo boy

模板字符串

**``**用反引号标识,在键盘1的左边。

  • 模板字符串可以使用插值表达式,展示变量
// 嵌入数值
const name = "kobe";
const msg = `hey, ${name}`;
  • 可以嵌入JS语句、JS表达式
const name = 'kobe'
const str = `this is ${name}`
console.log(str)
  • 支持换行、转义

模板字符串标签函数

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

const name = 'tom'
const gender = true
function myTagFunc (str, name, gender) {
  console.log(str, name, gender)  // [ 'hey, ', ' is a ', '.' ] tom true
  return str[0] + name + str[1] + gender + str[2]
}

const result = myTagFunc`hey, ${name} is a ${gender}.`
console.log(result) // hey, tom is a true.

字符串方法

  • .includes()
  • startsWith()
  • endsWith()
const message = 'Error: foo is not undefined.'

console.log(
  message.startsWith('Error'),
  message.endsWith('undefined.'),
	message.includes('foo')
)
// true true true

函数默认值

// 默认值没有传递或者传递的是undefined才会使用
// 带有默认值必须在最后,因为参数是从前到后传递的,放在前面就会被覆盖掉没办法正常工作!
// 否则返回true
function foo(enable = true) {
  console.log(enable)
}
foo(true) //true
foo(false) //false
foo() // true

剩余参数

  • 剩余参数是形参
  • 只有剩余参数的话可以接受全部的实参
// 只能出现在最后一位 只能使用一次
function foo(first, ...args) {
    console.log(args);
    // [ 2, 3, 4 ] 数组
}
foo(1, 2, 3, 4);

展开数组

三种方式:

const arr = ["foo", "bar", "baz"];
// 普通版
console.log(arr[0], arr[1], arr[2]);

// 进阶版
console.log.apply(console, arr);

// 展开数组版
console.log(...arr);

箭头函数

  • 箭头函数在this中可以当成没有,直接去找上一层的function!可以用于解决setTimeout的this指向window这样的情况!
const person = {
    name: "tom",
    sayHelloAsync: function () {
        setTimeout(() => {
            console.log(this.name);
        }, 1000);
    },
};
person.sayHelloAsync(); // tom
  • 只有一句return的时候花括号可以省略
  • 配合函数式编程
const arr = [1, 2, 3, 4, 5, 6, 7];
let newArr = arr.filter((i) => i % 2 == 0);
console.log(newArr);

对象字面量增强

  • 属性名和属性值相同时可以省略,只写属性名
  • 对象方法可以直接写函数形式:method1(){}
  • 使用方括号的方式计算动态属性名
const bar = 111
const obj = {
  foo: 123,
  // bar: bar,
  bar, // 同上一行效果
  // method1: function () {
  //   console.log(`method1: ${this}`)
  // },
  method2 () {
    // 直接写一个方法,同上面的冒号属性
    console.log(`method2: ${this}`)
  },
  [Math.random()]: 123, // 计算属性名
}
console.log(obj) // { foo: 123, bar: 111, method2: [Function: method2], '0.13076137144987743': 123 }

对象拓展方法

Object.assign

  1. 将多个源对象中的属性复制到一个目标对象中
  2. 类似于深拷贝
function func(obj) {
    const newObj = Object.assign({}, obj);
    newObj.name = "func newObj";
    console.log(newObj);
}
const obj1 = { name: "global obj1" };

func(obj1);
console.log(obj1);
// { name: 'func newObj' }
// { name: 'global obj1' }

Object.is

判断是否相等,==会在运算之前自动转换数据类型。===不会。

// 判断是否相等
console.log(0 == false);
// true
console.log(0 === false);
// false
console.log(NaN === NaN);
// false NaN应该是相等的
console.log(+0 === -0);
// true 无法判断正负号

console.log(Object.is(NaN, NaN), Object.is(+0, -0));
// true false

代理对象:Proxy

ES5中有一个Object.defineProperty,Vue2就是通过这个实现数据双向绑定

ES6提供了Proxy,可以监视对象的读写过程,Vue3.0通过Proxy实现数据绑定

用来替代Object.defineProperty(只能监视读取和写入)。功能更强大(delete操作或者对象方法的调用都可以监听到),使用也更方便。

  1. get监听属性的读取
  2. set监听属性的写入/修改
  3. deleteProperty监听delete
  4. set可以直接对于数组进行监视,defineProperty是重写数组的方法
const person = {
    name: "zs",
    age: 20,
};
const personProxy = new Proxy(person, {
    get(target, property) {
        // 返回值作为外部访问属性返回的结果
        return property in target ? target[property] : "default";
    },
    // 代理目标对象,属性名称,属性值,为代理目标设置指定的属性
    set(target, property, value) {
        // 设置校验
        if (property === "age") {
            if (!Number.isInteger(value)) {
                throw new TypeError(`${value} is not a int`);
            }
        }
    },
    deleteProperty(target, property) {
        console.log("delete", property);
        delete target[property];
    },
});

Proxy对比Object.defineProperty:

  • Object.defineProperty只能监听属性的读写
  • Proxy能监视更多对象操作:delete
  • Proxy更好的支持数组对象的监视(VUE重写数组的操作方法,劫持方法调用过程)

Reflect 统一的对象操作API

Reflect属于静态类(如Math),不能new,只能调用静态方法:Reflect.get()。Reflect内部封装了一系列对对象的底层操作。Reflect成员方法就是Proxy处理对象的默认实现

const proxy = new Proxy(obj, {
  get(target, property) {
    // 不写get逻辑,相当于调用Reflect.get(target, property)。
    return Reflect.get(target, property)
  }
})
  • Reflect统一提供一套用于操作对象的API
const obj = {
  foo: '111',
  bar: 'rrr',
  age: 18
}
// console.log("age" in obj)
// console.log(delete obj['bar'])
// console.log(Object.keys(obj))

console.log(Reflect.has(obj, 'name')) // false
console.log(Reflect.deleteProperty(obj, 'bar')) // true
console.log(Reflect.ownKeys(obj)) // [ 'foo', 'age' ]

Promise

一种更优的异步编程解决方案。解决了传统异步编程中回调函数嵌套过深的问题

详见异步编程

类 关键词 Class

class+名字+ {},constructor就是构造函数,可以用this访问实例对象。后面可以添加实例方法,static静态方法。

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

    say() {
        console.log(`hi, my name is ${this.name}`);
    }
    static create(name) {
    console.log(this); // class Person ... ...
    return new Person(name)
  }
}

继承extends

抽象出来相似的类型之间重复的地方

通过super.来访问父级的方法,通过super继承父级的属性。

class Student extends Person {
    constructor (name,number) {
        super(name)
        this.number = number
    }
    hello() {
        super.say()
    }
}

Set

  1. 增 add
  2. 删 delete
  3. 查 has
  4. 清空clear
  5. 长度 size

主要用于数组去重!

const result = [...new Set(arr)];

Map

  1. 增set键值对
  2. 删delete
  3. 查get键 / has
  4. 清空clear

Map 映射任意类型之间的关系. Map可以用任意对象作为键,而对象只能用字符串作为键

Symbol

字符串有坑重复但是Symbol不会!

最主要的作用就是为对象添加独一无二的属性名

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

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

console.log(Symbol('foo')) // Symbol(foo)
console.log(Symbol('bar')) // Symbol(bar)
console.log(Symbol('baz')) // Symbol(baz)

const obj = {}
obj[Symbol()] = 111
obj[Symbol()] = 2
console.log(obj) // { [Symbol()]: 111, [Symbol()]: 2 }


const name = Symbol()
const person = {
  [name]: 'jal', // 作为私有成员防止被访问
  say(){
    console.log(this[name])
  }
}
person.say()// jal
console.log(person[Symbol()]) // undefined
// console.log(person[name]) // jal

截止到ES2019一共定义了6种原始类型,和一个object类型,未来还会增加一个bigint的原始类型(stage-4阶段)标准化过后就是8种数据类型了

for…of循环

// for ... of 循环, 可以使用break
const arr = [1, 2, 3, 4]
for (const item of arr) { // item为每个对象实例
  console.log(item)
}
// 相当于
// arr.forEach(item => {
//   console.log(item)
// })

可以使用break终止循环

// arr.forEach ,但是这个方法不能终止遍历
// 为了终止遍历,我们之前,我们曾使用
// arr.some() 返回true
// arr.every() 返回false

for(const item of arr) {
  console.log(item) 
  if(item > 1)break
}

遍历集合Set

const s = new Set(['foo', 'bar'])
for(const item of s) {
  console.log(item)
}
// foo bar

遍历集合Map

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

for(const item of m) {
  console.log(item)
}
// [ 'foo', '123' ]  [ 'bar', '34' ]

// 解构键和值
for(const [key, value] of m) {
  console.log(key,value)
}
// foo 123
// bar 34

遍历对象,报错了:TypeError: obj is not iterable

const obj = {name: 'jal', age: 22}

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

ES中能够表示有结构的数据类型越来越多

Iterable接口(可迭代接口)

实现Iterable结构就是for…of的前提

  • 实现可迭代接口
// 迭代器 iterator 
const set = new Set(['foo', 'bar', 'baz'])

const iterator = set[Symbol.iterator]()

// 这就是for... of 循环实现的工作原理
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
// { value: 'foo', done: false }
// { value: 'bar', done: false }
// { value: 'baz', done: false }
// { value: undefined, done: true }
  • 实现迭代器原理:
// obj 实现可迭代接口 Iterable
const obj = {
  // iterator 方法
  [Symbol.iterator]: function () {
    // 迭代器接口 iterator 
    return {
      // 必须要有next方法
      next: function () {
        // 迭代结果接口 IterationResult
        return {
          value: 1,
          done: true
        }
      }
    }
  }
}
  • 具体实现:
const obj = {
  store: ['foo', 'bar', 'baz'],

  [Symbol.iterator]: function () {
    let index = 0
    const self = this

    return {
      next: function () {
        const result = {
          value: self.store[index],
          done: index >= self.store.length
        }
        index++
        return result
      }
    }
  }
}

for( const item of obj) {
  console.log(item)
}
// foo
// bar
// baz

上面就是设计模式中的迭代器模式。

小案例:

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '英语'],
  work: ['喝茶'],
  each: function (callback) {
    const all = [].concat(this.life, this.learn, this.work)
    for (const item of all) {
      callback (item)
    }
  },
  // 实现迭代器接口
  [Symbol.iterator]: function () {
    const all = [...this.life, ...this.learn, ...this.work]
    let index = 0
    return {
      next: function () {
        return {
          value: all[index],
          done: index++ >= all.length
        }
      }
    }
  }
}

todos.each(function (item) {
  console.log(item)
})
console.log('---------')
for(const item of todos) {
  console.log(item)
}

生成器函数 generator

避免异步编程中回调函数嵌套过深,提供更好的异步编程解决方案

function * foo () {
  console.log('zce')
  return 100
}
// 这个foo就是一个Generator函数

const result = foo()
console.log(result)// Object [Generator] {}
console.log(result.next())
// zce
// { value: 100, done: true }
// 可以看出生成器对象实现了Iterator接口

配合yield关键词使用。

生成器函数会返回一个生成器对象,调用这个生成器对象的next方法,才会让函数体执行,一旦遇到了yield关键词,函数的执行则会暂停下来,next函数的参数作为yield结果返回,如果继续调用函数的next函数,则会再上一次暂停的位置继续执行,直到函数体执行完毕,next返回的对象的done就变成了true

function * fn () {
  console.log(111)
  yield 100
  console.log(222)
  yield 200
  console.log(333)
  yield  300
}

const generator = fn()

console.log(generator.next())
// 111
// { value: 100, done: false }
console.log(generator.next())
// 222
// { value: 200, done: false }
console.log(generator.next())
// 333
// { value: 300, done: false }

案例1:发号器:

// Generator 应用: 发号器

function * createIdMaker () {
  let id = 1
  while(true) {
    yield id++
  }
}
const idMaker = createIdMaker()

console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// { value: 5, done: false }

案例2:

Generator函数实现迭代器Iterator
const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '英语'],
  work: ['喝茶'],

  // 实现迭代器接口
  [Symbol.iterator]: function * () {
    const all = [...this.life, ...this.learn, ...this.work]
    for (const item of all) {
      yield item
    }
  }
}

for(const item of todos) {
  console.log(item)
}
// 吃饭
// 睡觉
// 打豆豆
// 语文
// 数学
// 英语
// 喝茶

ES Modules

语言层面的模块化标准

ECMAScript 2016

数组的includes方法

const arr = ['foo', 1, false, NaN]
// 以前使用indexOf, 存在则返回下标,不存在则返回-1, 缺点是无法判断NaN
console.log(arr.indexOf('foo')) // 0
console.log(arr.indexOf(1)) // 1
console.log(arr.indexOf(false)) // 2
console.log(arr.indexOf(NaN)) // -1

console.log(arr.includes('foo')) // true
console.log(arr.includes(1)) // true
console.log(arr.includes(false)) // true
console.log(arr.includes(NaN)) // true

指数运算符:**

console.log(2 ** -52) // 2.220446049250313e-16

ECMAScript 2017

Object.values(obj)

获取对象所有的值数组

const obj = {
  name: 'jal',
  age: 20
}
// 对象的值组成的数组
console.log(Object.values(obj)) // [ 'jal', 20 ]

Object.entries(obj)

获取对象的键值数组

// 对象的键值数组, 可以for...of 这个对象了
console.log(Object.entries(obj)) // [ [ 'name', 'jal' ], [ 'age', 20 ] ]
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value)
}
// name jal
// age 20

console.log(new Map(Object.entries(obj))) // Map(2) { 'name' => 'jal', 'age' => 20 }

Object.getOwnPropertyDescriptors(obj)

获取对象的详细描述

const p1 = {
  firstName: 'Ji',
  lastName: 'Ailing',
  get fullName() {
    return this.firstName + ' '+ this.lastName
  }
}

const p2 = Object.assign({}, p1)
p2.firstName = 'zce'
console.log(p2) // { firstName: 'zce', lastName: 'Ailing', fullName: 'Ji Ailing' }
const descriptors = Object.getOwnPropertyDescriptors(p1)
console.log(descriptors)
/*
{
  firstName: { value: 'Ji', writable: true, enumerable: true, configurable: true },
  lastName: {
    value: 'Ailing',
    writable: true,
    enumerable: true,
    configurable: true
  },
  fullName: {
    get: [Function: get fullName],
    set: undefined,
    enumerable: true,
    configurable: true
  }
}
*/

const p3 = Object.defineProperties({}, descriptors)
p3.firstName = 'zce'
console.log(p3.fullName) // zce Ailing

padEnd/padStart

用指定字符串填充目标字符串的头部或者尾部,直到达到指定的长度为止

const books = {
  html: 5,
  css: 16,
  javascript: 128
}
for(const [key, value] of Object.entries(books)) {
  console.log(key, value)
}
// html 5
// css 16
// javascript 128

for(const [key, value] of Object.entries(books)) {
  console.log(`${key.padEnd(16, '-')}|${value.toString().padStart(3, '0')}`)
}
// html------------|005
// css-------------|016
// javascript------|128

在函数参数中添加尾逗号

添加逗号不会报错

function foo (
 bar, 
 baz,
) {
  
}

const arr = [
  10,
  20,
  30,
]

Async / Await

Generator的yield的语法糖,解决了回调地狱的问题。