Files
blog-press/docs/Web-Front/Others/JavaScript-Guide.md
2025-12-13 16:27:56 +08:00

54 KiB
Raw Blame History

JavaScript简介

JavaScript 实现

  完整的JavaScript实现包含以下几个部分:

  1. 核心(ECMAScript)
  2. 文档对象模型(DOM)
  3. 浏览器对象模型(BOM)

ECMAScript只是一个语言规范的标准,而JavaScript实现了这一标准。
  文档对象模型DOM(Document Object Model)是一个应用编程接口,用于在HTML中使用扩展的XMLDOM将整个页面抽象为一组分层节点。
  浏览器对象模型用于支持访问和操作浏览器的窗口。

语言基础

变量

var let const

  • let声明的范围是块作用域,var声明的范围是函数作用域。块作用域是函数作用域的子集。

varlet的区别:
var:函数作用域;存在变量提升;可重复定义;声明的变量会作为window的属性。
let:块级作用域;不存在变量提升(有暂时性死区);不可重复定义;声明的变量不会作为window的属性。
  块级作用域:即在{}花括号内的域,由{ }包括,比如if{}块、for(){}块。
  函数作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体都是有定义的。
  暂时性死区:在代码块中,在声明变量之前,该变量是不可用的。

  • const的行为与let基本相同,唯一的重要区别为声明变量的同时必须初始化变量。

  • 定义和声明变量时,const优先,let次之。

数据类型

ECMAScript变量包含两种类型的数据:原始值引用值
  原始值就是简单的数据类型,引用值则是由多个值构成的对象。保存原始值的变量是按值访问的,实际操作的就是存储在变量中的实际值。而引用值是保存在内存中的对象,JavaScript不允许直接访问内存位置,因此操作对象(引用值)时,实际操作的是对该对象的引用,因此保存引用值的变量是按引用访问的

原始值

原始值一共有6种Undefined、Null、Boolean、Number、String和Symbol

集合引用类型

Object

  • 对象字面量表达式中,属性名可以是字符串或数值,数值属性会自动转换为字符串。
  • 使用中括号可以通过变量访问属性,也可以在属性名中包括可能会导致语法错误的字符。
let person = {
  'total name': 'Cxx',
  5: true
}

console.log(person['total name'])

Array

  • from()用于将类数组结构转换为数组实例,of用于将一组参数转换为数组实例。
console.log(Array.from('Cxx'))

const a = [1, 2, 3, 4]
console.log(Array.from(a, x => x ** 2))

console.log(Array.of(1, 2, 3, 4, 'c', 'x', 'x'))

// 输出
// > ["C","x","x"]
// > [1,4,9,16]
// > [1,2,3,4,"c","x","x"]
  • 如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值+1。通过修改length属性,可以从数组末尾删除或添加元素。

  • 检测一个值是否为数组:Array.isArray()

  • keys()返回数组索引的迭代器,values()返回数组元素的迭代器,entries()返回索引/值对的迭代器。

const a = ['foo', 'bar']

// 对象解构
for (const [idx, element] of a.entries()) {
    console.log(idx, element)
}

// 输出
// > 0,foo
// > 1,bar

数组方法

基本方法

  • 栈方法:
// 接收任意数量的参数,并将其添加至数组末尾,返回数组的最新长度
arrayLength = array.push(...item)

// 删除数组的最后一项并减少数组的length值返回被删除的项
deleteItem = array.pop()
  • 队列方法:
// 删除数组的第一项并返回它,然后数组长度-1
deleteItem = array.shift()

// 在数组开头添加任意多个值,然后返回新的数组长度
arrayLength = array.unshift(...item)
  • 排序方法:
// 将数组元素反向排列
array = array.reverse()

// 按照升序重新排列数组元素
// 会先对每一项调用 String() 转换为字符串,再进行排序
array = array.sort([compareFunction])

// 示例
const a = [0, 1, 5, 10, 15]

function compare(value1, value2) {
  if (value1 < value2) {
    return -1
  } else if (value1 > value2) {
    return 1
  } else {
    return 0
  }
}

console.log(a.sort())
console.log(a.sort(compare))

// 输出
// > [0,1,10,15,5]
// > [0,1,5,10,15]

操作方法

  • concat()
// 将其参数添加至副本末尾 然后返回这个新数组
newArray = array.concat(item/array)

// 示例
const colors1 = ['red', 'green']
const colors2 = ['black', 'white']

console.log(colors1.concat(colors2))

// 是否打平数组
colors2[Symbol.isConcatSpreadable] = false
console.log(colors1.concat(colors2))

// 输出
// > ["red","green","black","white"]
// > ["red","green",["black","white"]]
  • slice():切片
// 参数:返回元素的开始索引和结束索引 返回值:对应索引的元素
newArray = array.slice(startIndex, [endIndex])

// 示例
const colors = ['red', 'green', 'black', 'white']

console.log(colors.slice(2))
// 不改变原数组
console.log(colors)
console.log(colors.slice(1, 3))

// 输出
// > ["black","white"]
// > ["red","green","black","white"]
// > ["green","black"]
  • splice():拼接
// 参数:索引值 需要删除的个数(可以为0) 需要插入的元素(可以为0)
// 返回值:从数组中删除的元素
deleteArray = array.splice(index, deleteNum, [insertItem])

const colors = ['red', 'green', 'black', 'white']

// 删除元素
const deleteArray = colors.splice(0, 1)
console.log(deleteArray)
console.log(colors)
console.log('**********')

// 插入元素
const insertArray = colors.splice(1, 0, 'yellow')
console.log(insertArray)
console.log(colors)
console.log('**********')

// 替换元素
const replaceArray = colors.splice(1, 1, 'orange')
console.log(replaceArray)
console.log(colors)

// 输出
// > ["red"]
// > ["green","black","white"]
// > **********
// > []
// > ["green","yellow","black","white"]
// > **********
// > ["yellow"]
// > ["green","orange","black","white"]

搜索方法

indexOf()lastIndexOf()includes()

// 参数:要查找的元素,搜索位置 返回值:查找元素在数组的位置
// 从第一项向后搜索
index = indexOf(searchItem, [searchInedex])

// 从最后一项向前搜索
index = lastIndexOf(searchItem, [searchInedex])

// 参数:同上 返回值:是否找到一个与指定元素匹配的项
isInclude = includes(searchItem, [searchInedex])

  自定义搜索:find()findIndex()

// 参数:断言函数(元素、索引和数组本身) 返回值:匹配项
item = array.find((element, index, array) => { ... })

// 参数:同上 返回值:匹配项的索引
itemIndex = array.findIndex((element, index, array) => { ... })

// 示例
const people = [
  {
    name: 'Cxx',
    age: 27
  },
  {
    name: 'Pjm',
    age: 25
  }
]

console.log(people.find((element, index, array) => element.age > 26))
console.log(people.findIndex((element, index, array) => element.age > 26))

// 输出
// > {"name":"Cxx","age":27}
// > 0

迭代方法

every()filter()forEarch()map()some()。每个方法都接收一个以每一项为参数运行的函数。该函数接收3个参数数组元素、元素索引和数组本身。

  • every():如果每一项返回true,则返回true
  • some():如果某一项返回true,则返回true
let numbers = [1, 2, 3, 4, 5]

console.log(numbers.every((item, index, array) => item >  3))
console.log(numbers.some((item, index, array) => item >  3))

// 输出
// > false
// > true
  • filter():返回每次函数调用的结果为true的项组成的数组
  • map():返回每次函数调用的结果组成的数组
let numbers = [1, 2, 3, 4, 5]

console.log(numbers.filter((item, index, array) => item >  3))
console.log(numbers.map((item, index, array) => item *  3))
  • forEach():循环遍历,不返回结果
let numbers = [1, 2, 3, 4, 5]

numbers.forEach((item, index, array) => {
    console.log(index, item)
})

// 输出
// > 0,1
// > 1,2
// > 2,3
// > 3,4
// > 4,5

Map

基本API

  使用newMap构造函数就可以创建一个空映射,也可以给**Map构造函数传入一个可迭代对象进行实例化。**
Map可以使用任何JavaScript数据类型作为键。
  映射实例可以提供一个迭代器,并且会维护键值对的插入顺序。

// 创建空映射
const m = new Map()

// 使用数组初始化映射
const m1 = new Map([['key1', 'value1'], ['key2', 'value2']])
// 使用迭代器初始化映射
const m2 = new Map({
  [Symbol.iterator]: function* () {
    yield ['key1', 'value1']
    yield ['key2', 'value2']
  }
})

// 常用API
console.log(m1.has('key1')) // 查询
console.log(m1.get('key3')) // 查询
m1.delete('key1') // 删除某个键
console.log(m1.get('key'))
m1.set('key3', 'value3') // 增加某个键
console.log(m1.get('key3'))
m1.clear()
console.log(m1.size) // 获取键值对数量
console.log('**********')

// Map迭代 API同迭代器
for (const pair of m2.entries()) {
  console.log(pair)
}

// 输出
// > true
// > undefined
// > undefined
// > value3
// > 0
// > **********
// > ["key1","value1"]
// > ["key2","value2"]

Object与Map区别

Object Map
内存占用
插入性能
查询速度
删除性能

WeakMap

  • 弱映射键只能是Object或者继承自Object的类型。
  • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的,不能遍历。

Set

基本API

// 创建空映射
const s = new Set()

// 使用数组初始化映射
const s1 = new Set(['value1', 'value2'])
// 使用迭代器初始化映射
const s2 = new Set({
  [Symbol.iterator]: function* () {
    yield 'value1'
    yield 'value2'
  }
})

// 常用API
console.log(s1.has('value1')) // 查询
s1.delete('value1') // 删除某个值
console.log(s1.has('value1'))
s1.add('value3') // 增加某个值
console.log(s1.has('value3'))
s1.clear()
console.log(s1.size) // 获取元素数量
console.log('**********')

// Set迭代 API同迭代器
for (const value of s2.values()) {
  console.log(value)
}

// 输出
// > true
// > false
// > true
// > 0
// > **********
// > value1
// > value2

WeakSet

  • 成员只能是Object或者继承自Object的类型。
  • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏。
  • 不能遍历。

总结

Map是键-值对,Set是值-值对。

迭代器与生成器

迭代器

迭代器概念

  可迭代对象:实现了正式的Iterable接口,而且可以通过迭代器Iterator消费。
  实现Iterable接口:需要同时具备支持迭代的自我识别能力和创建实现Iterator接口的对象的能力。(也即必须暴露一个Symbol.iterator属性作为默认迭代器)
  迭代器:是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现Iterable接口的对象都有一个Symbol.iterator属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也是一个函数,调用之后就会产生一个实现Iterator接口的对象。
  迭代器是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API
实现Symbol.iterator方法的对象就是可迭代对象,该方法返回一个迭代器。实现了next方法的对象就是迭代器 可迭代对象和迭代器可以合并为一个对象,即同时实现Symbol.iteratornext方法。
  可迭代对象是按一定规则建立好的对象(函数或者其他),迭代器则是需要的时候实例化即可,每个迭代器都拥有一定的API

迭代器使用

  接收可迭代对象的原生语言特性:
1.for-of循环 2.数组解构 3.扩展操作符 4.Array.from() 5.创建集合 6.创建映射 7.yield*操作符,在生成器中使用。
  迭代器API使用next()在可迭代对象中遍历数据,返回done(true/false)value值。
  只要迭代器到达done:true状态,后续调用next()就一直返回同样的值。不同迭代器的实例相互之间没有联系,只会独立遍历可迭代对象。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化。

原生可迭代对象类型的迭代器

let arr = ['foo', 'bar']

// arr[Symbol.iterator] 是迭代器工厂函数
// 调用该函数即可生成 迭代器
let iter = arr[Symbol.iterator]()
console.log(iter.next())

  实现Iterable接口的内置类型:
1.字符串 2.数组 3.映射 4.集合 5.arguments对象 6.NodeListDOM集合类型

自定义迭代器

  在函数/对象中实现next()接口即可构成迭代器,但此时只能调用迭代器API,无法使用可迭代对象的特性,如for-of循环。

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    // 实现 next 方法
    next: function() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false }
        nextIndex += step;
        iterationCount++;
        // 必须返回 value done 结构的对象
        return result;
      }
      return { value: iterationCount, done: true }
    }
  };
  return rangeIterator;
}

// 调用迭代器函数
let iterator = makeRangeIterator(1, 10, 2);

// 可以使用迭代器API
let result = iterator.next();
while (!result.done) {
  console.log(result.value);
  result = iterator.next();
}

// 无法使用可迭代对象的特性
for (let it of iterator) {
  console.log(it)
}

// 输出
// 1
// 3
// 5
// 7
// 9
// Uncaught TypeError: iterator is not iterable

自定义可迭代对象

  如果需要使用可迭代对象的特性,则需将其改为对象模式,并实现Symbol.iterator属性。可以使用class构造,也可以使用对象字面量的形式:

class Counter {
  constructor(limit) {
    this.limit = limit
  }

  // 实现 Symbol.iterator 属性
  [Symbol.iterator]() {
    let count = 1, limit = this.limit

    // 需要在该属性中返回一个迭代器
    // 也即需要实现 next 方法
    return {
      // 实现 next 方法
      next() {
        if (count <= limit) {
          // 返回 done 和 value 属性值
          return { done: false, value: count++ }
        } else {
          return { done: true }
        }
      }
    }
  }
}

let counter1 = new Counter(5)

for (let i of counter1) {
    console.log(i)
}

// 输出
// > 1
// > 2
// > 3
// > 4
// > 5

  这里使用闭包(将next()方法放在了return语句中,并且在实例化时,仍然可以访问count变量)保存了count变量,使得可以同时创建多个迭代器。

let makeRangeIterator =  {
  [Symbol.iterator]() {
    start = 1, end = 10, step = 2

    let nextIndex = start;
    let iterationCount = 0;

    const rangeIterator = {
      next: function() {
        let result;
        if (nextIndex < end) {
          result = { value: nextIndex, done: false }
          nextIndex += step;
          iterationCount++;
          return result;
        }
        return { value: iterationCount, done: true }
      }
    };
    return rangeIterator;
  }
}

for (let it of makeRangeIterator) {
  console.log(it)
}

// 输出
// 1
// 3
// 5
// 7
// 9

生成器

生成器概念

  在函数名称前面加上星号(*)表示它是一个生成器(箭头函数不能用来定义生成器函数),调用生成器函数会产生一个生成器对象。
生成器对象内部实现了Iterator接口,一开始处于暂停执行状态,调用next()方法可以让生成器开始或恢复执行。
next()方法中的value属性是生成器函数的返回值,生成器函数只会在初次调用next()方法后开始执行。
  使用生成器可以快速的构造迭代器,只需要在内部编写逻辑即可:
  例如将之前的示例改为使用生成器构造:

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

let iterator = makeRangeIterator(1, 10, 2)
// 可以使用迭代器API
console.log(iterator.next())

// 可以使用可迭代对象的特性
for (let it of iterator) {
  console.log(it)
}

// 输出
// {value: 1, done: false}
// 3
// 5
// 7
// 9

yield关键字

yield关键字可以让生成器停止和开始执行。**生成器函数在遇到yield关键字之前会正常执行遇到之后会停止函数作用域会保留调用next()后会恢复执行。**通过yiel关键字退出的生成器函数会处在done:false状态,通过return关键字退出的生成器会处在done:true状态。
yield关键字必须只能位于生成器函数中定义。

function* generatorFn() {
  yield 'foo'
  yield 'bar'
  return 'baz'
}

let generatorObject = generatorFn()

console.log(generatorObject.next())
console.log(generatorObject.next())
console.log(generatorObject.next())

// 输出
// > {"value":"foo","done":false}
// > {"value":"bar","done":false}
// > {"value":"baz","done":true}

生成器应用

// 生成器对象作为可迭代对象
function* generatorFn1() {
  yield 1
  yield 2
  yield 3
}

// 使用星号增强yield
function* generatorFn2() {
  yield*  [1, 2, 3]
}

// 生成器作为默认迭代器
class Foo {
  constructor() {
    this.values = [1, 2, 3]
  }

  // 定义为生成器函数
  *[Symbol.iterator] () {
    yield* this.values
  }
}

for (const x of generatorFn1()) {
  console.log(x)
}

for (const x of generatorFn2()) {
  console.log(x)
}

const f = new Foo()
for (const x of f) {
  console.log(x)
}

// 输出值均相同
// > 1
// > 2
// > 3

总结

  实现[Symbol.iterator]属性的对象就是可迭代对象,可迭代对象就可以使用for-of等原生的语言特性,实现next()方法的就是迭代器,就可以使用相应的API。在函数名称前加上*就是生成器,生成器内部实现了迭代器的接口,因此也可以使用迭代器的API,但同时也具有yield等特殊特性。
  创建迭代器的方法有以下几种:

  1. 使用原生具体可迭代对象性质的数据类型,如:
const str = 'abc'
const strIterator = str[Symbol.iterator]()
console.log(strIterator.next())
  1. 自定义具有[Symbol.iterator]属性和next()方法的对象/函数。

  2. 使用生成器

  本质上相对于可以自定义数据类型,创建具有类似于数组功能的数据结构,使用更改灵活。

参考资料

  1. ES6中的迭代器(Iterator)和生成器(Generator)

对象、类

  对象为一组属性的无序集合,可以将其想象为一张散列表,其中内容就是一组名/值对,值可以是数据或者函数

对象属性

  为对象的数据添加一些额外的属性效果,可以使用Object.defineProperty(Object, keyName, options)定义,也可以同时定义多个(使用Object.definePropertys(Object, options))。
  使用Object.getOwnPropertyDescriptor(Object, keyName)可以查看指定属性的描述符,也可以使用Object.getOwnPropertyDescriptors(Object)查看所有的属性描述符。

// 定义对象,包含一个直接定义在对象上的属性
let book = {year_: 2017}

// 添加对象属性
Object.defineProperties(book, {
  edition: {
    // 数据属性
    value: 1
  },

  year: {
    // 访问器属性
    // 获取函数 在读取属性时调用
    get() {
      return this.year_
    },
    // 设置函数 在写入属性时调用
    set(newValue) {
      if (newValue > 2017) {
        this.year_ = newValue
      }
    }
  }
})

// 打印对象属性信息
console.log(Object.getOwnPropertyDescriptors(book))
const descriptor = Object.getOwnPropertyDescriptor(book, "year")
// 打印访问器属性,注意这里是属性,不能使用函数调用()
console.log(typeof descriptor.get)

  输出结果:

{
  "year_": {
    // 直接定义在对象上的属性特性默认为true
    // 属性实际值
    "value": 2017,
    // 属性的值是否可以被修改
    "writable": true,
    // 属性的值是否可以被枚举,即可以通过 for-in 返回
    "enumerable": true,
    // 属性的值是否可以被配置
    "configurable": true
  },
  "edition": {
    "value": 1,
    "writable": false,
    "enumerable": false,
    "configurable": false
  },
  "year": {
    "enumerable": false,
    "configurable": false
  }
} 

"function" 

ES6增强的对象语法

let name = 'cxx'
const ageKey = 'age'

let person = {
  // 属性值简写 相当于 name:name
  name,
  // 可计算属性 []表示对象的属性键将其作为表达式运算
  [ageKey]: 18,
  job: 'engineer',
  // 方法名简写 相当于 sayName: function(){}
  sayName() {
    console.log(name)
  },
  get name() {
    return `My name is ${name}`
  }
}

console.log(person.name)
person.sayName()

// 对象解构
let { job } = person
console.log(job)

// 输出
> My name is cxx
> cxx
> engineer

创建对象

Object构造函数

let person = new Object()

person.name = 'Cxx'
person.age = 18
person.job = 'Software Engineer'
person.sayName = function() {
  console.log(this.name)
}

对象字面量

let person = {
  name: 'Cxx',
  age: 18,
  job: 'Software Engineer',
  sayName() {
    console.log(this.name)
  }
}

  以上两种方式创建对象比较方便,但是无法创建具有同样接口的多个对象。

工厂模式

function createPerson(name, age, job) {
  let o = new Object()
  o.name = name
  o.age = age
  o.job = job

  o.sayName = function() {
    console.log(this.name)
  }

  return o
}

let person1 = createPerson('cxx1', 18, "Student")
let person2 = createPerson('cxx2', 17, "Student")

  可以解决创建多个类似对象的问题,但是无法确定新创建对象的类型。

构造函数模式

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function() {
    console.log(this.name)
  }
}

let person1 = new Person('cxx1', 18, "Student")
let person2 = new Person('cxx2', 17, "Student")

console.log(person1.constructor)
console.log(person1.__proto__)
console.log(person1.prototype)
console.log('----------------')
console.log(Person.constructor)
console.log(Person.__proto__)
console.log(Person.prototype)


// 输出
> function Person(name, age, job) { this.name = name this.age = age this.job = job this.sayName = function() { console.log(this.name) } }
> {}
> undefined
> ----------------
> function Function() { [native code] }
> function () { [native code] }
> {}

  这里使用了构造函数(Person())来代替之前的工厂函数。
  同时使用new操作符来创建Person的实例,以这种方式创建时,会执行以下步骤:

  1. 在内存重创建一个新实例对象。(即person1person2)
  2. 在新实例对象内部的[[Prototype]](也即__proto__属性)特性被赋值为构造函数的prototype属性。(即函数的prototype属性等于实例对象的__proto__属性:Person.prototype === person1.__proto__)
  3. 构造函数内部的this被赋值为这个新的实例对象。(this指向新对象,即运行时赋值,this.name即新实例对象的name)
  4. 执行构造函数内部的代码。(给新的实例对象添加属性和方法)
  5. 如果构造函数返回非空对象,则返回改对象,否则,返回刚创建的对象。

这里需要记住2点__proto__constructor属性是对象所独有的;②prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__constructor属性。
constructor属性是用来标识对象类型的,始终指向创建当前对象的构造函数。
  任何函数只要使用new操作符调用就是构造函数。
  然而使用构造函数创建对象仍然存在问题,因为在JavaScript中,函数也是对象。因此在创建sayName()函数时,本质上相当于:

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = new Function(console.log(this.name))
}

let person1 = new Person('cxx1', 18, "Student")
let person2 = new Person('cxx2', 17, "Student")

console.log(person1.sayName === person2.sayName)

// 输出
> false

  因为此时每个Person实例都会有自己的Function实例所以这2个同名函数实际上并不相等。

原型模式

  每个函数都会创建一个prototype属性,该属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法,这个对象也是通过调用构造函数创建的对象的原型。

function Person() {}

Person.prototype.name = 'Cxx'
Person.prototype.age = 18
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function() {
    console.log(this.name)
}

let person1 = new Person()

let person2 = new Person()

console.log(person1.sayName === person2.sayName)

// 输出
> true

  所有的属性和方法都直接添加到Personprototype属性上了,使用这种原型模式定义的属性和方法是由所有的实例对象共享的。

深入理解原型

理解原型   在JavaScript中,只要创建了一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。所有的原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数
  在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object(本例中的其他方法是在后面自定义的)。每次调用构造函数会创建一个新实例对象,这个实例对象的内部[[Prototype]]指针就会被赋值为构造函数的原型对象(大部分浏览器将这个指针定义为实例对象上的__proto__属性)。通过这个属性就可以访问对象的原型。
  因此,实例对象通过__proto__可以链接到原型对象,它实际上指向隐藏特性[[Prototype]],构造函数通过Prototype属性链接到原型对象。实例对象与构造函数原型之间有直接的联系,但实例对象与构造函数之间没有关系。
  在通过对象访问属性时,**如果没有找到这个属性,则搜索会沿着指针进入原型对象。**只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性。
  只要通过对象可以访问属性或方法,in操作符就返回true(例'name' in person1),而hasOwnProperty()只有属性存在于实例上才返回true

  附:参考资料:

  1. 帮你彻底搞懂JS中的prototype、__proto__与constructor图解
  2. JS中prototype介绍

对象迭代

Object.values()返回对象值的数组,Object.entries()返回键值对的数组。

注意事项

function Person() {}

Person.prototype = {
  name: 'Cxx',
  age: 18,
  job: 'Software Engineer',
  sayName() {
    console.log(this.name)
  } 
}

// 以字面量形式定义原型对象时,需要恢复 constructor 属性
// 且将其设置为不可枚举
Object.defineProperty(Person.prototype, 'constructor', {
  enumerable: false,
  value: Person
})

let friend = new Person()

// 可以在原型对象上添加属性和方法
// 但不推荐在原生的原型对象上添加
// 可以创建一个自定义类 使其继承原生类型
Person.prototype.sayHi = function() {
  console.log('hi')
}

friend.sayHi()

// 输出
// > hi

// 重写整个原型会切断最初原型与构造函数的联系
// 但实例对象引用的仍然是最初的原型
// 因此下面的`sayHello()`调用会报错
Person.prototype = {
  sayHello() {
    console.log('hello')
  } 
}

friend.sayHello()
// 输出
// > Uncaught TypeError: friend.sayHello is not a function

  通过原型模式创建对象也存在问题:

  1. 弱化了向构造函数传递初始化参数的能力
  2. 所有实例对象可以共享原型对象的属性和方法

继承

  构造函数、原型对象和实例对象之间的关系:每个构造函数都有一个原型对象,原型对象有一个属性指回构造函数,而实例有一个内部指针指向原型对象。
  原型链:通过将原型对象赋值为另一个类型的实例。

function SuperType() {
  this.property = true
}

SuperType.prototype.getSuperValue = function() {
  return this.property
}

function SubType() {
  this.subproperty = false
}

// 继承 SuperType
// 将原型对象赋值为另一个类型的实例
SubType.prototype = new SuperType()

SubType.prototype.getSubValue = function() {
  return this.subproperty
}

let instance = new SubType()

// 访问继承的方法
console.log(instance.getSuperValue())

  通过继承,SubType的实例不仅能从SuperType的实例中继承属性和方法,而且还与SuperType的原型挂钩,同时也可以扩展原型搜索机制,搜索原型的原型。
  所有的引用类型都继承自Object,任何函数的默认原型都是一个Object的实例对象。
  子类需要覆盖或者添加父类的方法时,必须在原型赋值之后再添加到原型上。
以对象字面量形式创建原型方法会破坏之前的原型链,相当于重写了原型链。

  类是ES6新的基础性语法糖结构,本质上仍是原型和构造函数的概念。

基础知识

// 表达式声明
const Animal = class {}

// 类就是一种特殊函数
console.log(typeof Animal)
console.log('----------')

// 输出
// > function

// 类声明
class Person1 {
  constructor(name) {
    console.log(arguments.length)
  }
}

// 如果不需要参数,类名后面的括号可以省略
let p1 = new Person1
console.log(p1.name)
console.log('----------')

// 输出
// > 0
// > undefined

class Person2 {
  constructor(override) {
    this.foo = 'foo'
    if (override) {
      // 默认返回 this 对象
      // 也可以返回其他对象
      return {
        bar: 'bar'
      }
    }
  }
}

let p2 = new Person2(true)
console.log(p2)
console.log('----------')

// 输出
// > {"bar":"bar"}

class Person3 {
  constructor() {
    // 添加到 this 的所有内容都会存在于不同的实例上
    this.locate = () => {
      console.log('instance')
    }
  }

  // 定义在类的原型对象上
  locate() {
    console.log('prototype')
  }

  // 定义在类本身上
  static locate() {
    console.log('class')
  }
}

let p3 = new Person3()
p3.locate()
Person3.prototype.locate()
Person3.locate()

// 输出
// > instance
// > prototype
// > class

  类可以包括构造函数方法、实例方法、获取函数、设置函数和静态类方法。
constructor关键字用于在类定义块内部创建类的构造函数,在使用new操作符创建类的实例时,会调用该函数。
  在JavaScript中,类就是一种特殊的函数,因此类也有prototype属性,该原型对象也有一个constructor属性指向类本身。
  类是JavaScript的一等公民,也可以像其他对象或函数引用一样把类作为参数传递。
  每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享。

继承

ES6类支持单继承,使用extends关键字。类和原型上定义的方法都会被带到派生类。

Super关键字

class Vehicle {
  constructor() {
    this.hasEngine = true
  }

  print() {
    console.log(this.hasEngine)
  }

  static identify() {
    console.log('vehicle')
  }
}

// extends 继承
class Bus extends Vehicle {
  constructor() {
    // 调用父类构造函数
    // super 作为函数调用
    super()
    // 调用父类原型对象上的方法
    // super 作为对象调用
    super.print()
  }

  static identify() {
    // 调用父类的静态方法 指向父类
    super.identify()
  }
}

const bus = new Bus()
Bus.identify()

// 输出
// > true
// > vehicle

super既可以当函数使用,也可以当对象使用。子类的构造函数必须执行一次super函数。
  不能单独引用super关键字,不能在调用super()之前引用this

函数

  函数实际上也是对象,每个函数都是Function类型(属于引用类型)的实例。Function也有属性和方法。
JavaScript引擎在任何代码执行前,会先读取函数的声明。

箭头函数

  箭头函数不能使用argumentssupernew.target,也不能用作构造函数,箭头函数也没有prototype属性。

函数名

  函数名就是指向函数的指针,一个函数可以拥有多个名称。使用不带括号的函数名会访问函数指针,而不会执行函数。
ES6中所有的函数对象都会暴露一个只读的name属性,包含函数的信息。

函数参数

// 扩展运算符收集参数
function doAdd(num = 5, ...nums) {
  if (arguments.length === 1) {
    console.log(num)
  } else if (arguments.length === 2) {
    // 此时 num 默认值会被覆盖
    console.log(num)
    console.log(arguments[0])
    // 其他参数被收集到 nums 中
    console.log(nums)
  }
}

let values = [1, 2]

// 扩展运算符传值
doAdd(...values)

// 输出
// > 1
// > 1
// > [2]

  在JavaScript中,函数参数只是为了方便写出来,并不是必须写出来,并没有验证命名参数的机制,因此传递任意数量的参数都是可以的。
arguments对象是一个类数组对象,存放着函数的参数信息。ES6中可以显示定义默认参数。函数的默认参数只有在函数调用时才会求值,不会在函数定义时求值。
  使用扩展运算符在收集参数时,只能把它作为最后一个参数。

函数内部

this

  在标准函数中,this引用的是把函数当成方法调用的上下文对象,在箭头函数中,this引用的是定义箭头函数的上下文。

function King () {
  this.royaltyName = 'Henry',
  console.log(this.royaltyName)

  setTimeout(() => {
    console.log(this.royaltyName)
  }, 1000)

  setTimeout(function() {
    console.log(this.royaltyName)
  }, 1000)
}

new King()

// 输出
// > Henry
// > Henry
// > undefined

  在利用函数创建类时,如果使用普通函数,则无法访问到内部的this值(此时作用域位于Window)。

函数方法

  函数除了可以使用函数名+()的形式调用,也可以使用apply()call()方法调用这2个方法的第一个参数均可以指定函数内部的this,调用apply()方法时,传入的参数需要为一个数组,而调用call()方法时,需要将参数一一列出。

window.color = 'red'

let o = {
  color: 'blue'
}

function sayColor(a, b) {
  console.log(a, b)
  console.log(this.color)
}

// 正常调用的 this 为 Window对象
sayColor(1, 2)

console.log('**********')
// 第2个参数为数组形式
sayColor.apply(window, [1, 2])
sayColor.apply(o, [1, 2])

console.log('**********')
// 需要将参数一一列出
sayColor.call(window, 1, 2)
sayColor.call(o, 1, 2)

// 输出
// > 1,2
// > red
// > **********
// > 1,2
// > red
// > 1,2
// > blue
// > **********
// > 1,2
// > red
// > 1,2
// > blue

函数作为值

  可以把函数作为参数传给另一个函数,也可以在一个函数中返回另一个函数。(实际上就是把函数名当指针使用)

闭包

  闭包指的是引用了另一个函数作用域变量的函数,通常是在嵌套函数中实现的。

// 正常函数
function compare(value1, value2) {
  if (value1 < value2) {
    return -1
  } else if (value1 > value2) {
    return 1
  } else {
    return 0
  }
}

let result = compare(5, 10)

普通函数作用域链
  如图所示,compare()函数是在全局上下文中调用的,第一次调用时,会为它创建一个包含argumentsvalue1value2的活动对象,该对象是其作用域链上的第一个对象,而全局上下文的变量对象则是compare()作用域链上的第二个对象,包含thisresultcompare
  全局上下文会在代码执行期间始终存在,而函数局部上下文只在函数执行期间存在。
  在定义compare()函数时,会为其创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链,接着会创建函数的活动对象并将其推入作用域链的前端。
  函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中只剩下全局作用域。
  然而,闭包中就不一样了。

// 闭包函数
function createComparisonFunction(propertyName) {
  return function(object1, object2) {
    let value1 = object1[propertyName]
    let value2 = object2[propertyName]

    if (value1 < value2) {
      return -1
    } else if (value1 > value2) {
      return 1
    } else {
      return 0
    }
  }
}

let compare = createComparisonFunction('name')
let result = compare({'name': 'Cxx'}, {'name': 'Pjm'})

闭包函数作用域链
在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。
  在createComparisonFunction()返回匿名函数后,其作用域链会被初始化为包含createComparisonFunction()的活动对象和全局变量对象,这样匿名函数就可以访问到createComparisonFunction()可以访问到的所有变量。
  而当createComparisonFunction()执行完毕后,匿名函数仍然有它的引用,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁。

闭包的作用

闭包常常用来间接访问一个变量,或者隐藏一个变量。(使用全局变量也可以满足,但是会导致变量变为公有,不能起到一定的保护作用。)

附:关于闭包的形式解释

  这里有一个小故事,可以更好的理解函数闭包的含义:公主的故事
  有一个公主...

function princess() {

  她生活在一个充满冒险的奇妙世界。她遇到了她的白马王子,骑着独角兽环游世界,与龙搏斗,遇到会说话的动物,以及许多其他奇幻事物。

var adventures = [];

function princeCharming() { /* ... */ }

var unicorn = { /* ... */ },
    dragons = [ /* ... */ ],
    squirrel = "Hello!";

/* ... */

  但她总是不得不回到她枯燥的家务和成年人的世界。

  return {

  她经常告诉他们她作为公主的最新惊人冒险。

    story: function() {
      return adventures[adventures.length - 1];
    }
  };
}

  但他们看到的只是一个小女孩……

var littleGirl = princess();

...讲述关于魔法和幻想的故事。

littleGirl.story();

  即使大人们知道真正的公主,他们也永远不会相信独角兽或龙,因为他们永远看不到它们。大人们说,他们只存在于小女孩的想象中。
  但我们知道真相;那个带着公主的小女孩……
  ……真是个公主,里面有一个小女孩。

Princess()函数是一个包含私有数据的复杂作用域。在函数之外,不能看到或访问私有数据。公主把独角兽、龙、冒险等都留在了她的想象中(私人数据),大人自己看不到。但是公主的想象力被story()函数的闭包捕捉到了,这是littleGirl实例暴露给魔法世界的唯一接口。

参考资料

  1. 闭包MDN
  2. JS 中的闭包是什么?
  3. 关于闭包

代理与反射

  代理用于定义基本操作的自定义行为,本质属于元编程(meta programming),可以修改程序的默认形为,就形同于在编程语言层面上做修改。
  元编程,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。
  代理是目标对象的抽象,可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。

代理基础

const target = {
  foo: 'bar'
}

const handler = {
  // 捕获器键值为函数
  // 接收三个参数 目标对象 要查询的属性 代理对象
  get: function(trapTarget, property, receiver) {
    console.log('trapTarget:', trapTarget)
    console.log('property:', property)
    console.log('receiver:', receiver)
    return trapTarget[property] + ' get handler'
  },
  // ES6 对象方法简写
  set() {
    console.log('set handler')
    // 反射API
    Reflect.set
  }
}

// 接收两个参数 目标对象 处理程序对象 
// 注:这里是对象 不是方法
const proxy = new Proxy(target, handler)

console.log(target.foo)
target.foo = 'foo'
console.log('**********')
// 需要对 Proxy 对象进行操作 才会触发代理
console.log(proxy.foo)
proxy.foo = 'foo'


// 输出
// bar
// **********
// trapTarget: {foo: "foo"}
// property: foo
// receiver: Proxy {foo: "foo"}
// foo get handler
// set handler
  • 代理使用Proxy构造函数创建,该函数接收两个参数:目标对象和处理程序对象。

  • 在代理对象上执行的任何操作实际上都会应用到目标对象,每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

  • 只有在代理对象上执行这些操作才会触发捕获器,在目标对象上执行这些操作仍然会产生正常的行为。

捕获器 API Object API
handler.getPrototypeOf() Object.getPrototypeOf()
handler.setPrototypeOf() Object.setPrototypeOf()
handler.isExtensible() Object.isExtensible()
handler.preventExtensions() Object.preventExtensions()
handler.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor()
handler.defineProperty() Object.defineProperty()
handler.has() in 操作符
handler.get() 属性读取操作
handler.set() 属性设置操作
handler.deleteProperty() delete 操作符
handler.ownKeys() Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()
handler.apply() 函数调用操作
handler.construct() new 操作符

handler 对象的方法

const target = {
  foo: 'bar'
}
const handler = {
  get(trapTarget, property, receiver) {
    // 通过反射API简化代码
    return Reflect.get(...arguments)
  }
}

// 对象解构
const { proxy, revoke } = Proxy.revocable(target, handler)
console.log(proxy.foo)
// 撤销代理
revoke()
console.log(proxy.foo)

// 输出
// bar
// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

  处理程序对象中所有可以捕获的方法都有对应的反射API方法,可以通过调用反射API实现简化代码。

Reflect API Object API 功能/作用
Reflect.apply(target, thisArgument, argumentsList) Function.prototype.apply() 对一个函数进行调用操作,同时可以传入一个数组作为调用参数
Reflect.construct(target, argumentsList[, newTarget]) new target(...args) 对构造函数进行 new 操作
Reflect.defineProperty(target, propertyKey, attributes) Object.defineProperty() 如果设置成功就会返回 true
Reflect.deleteProperty(target, propertyKey) delete target[name] 作为函数的delete操作符
Reflect.get(target, propertyKey[, receiver]) target[name] 获取对象身上某个属性的值
Reflect.getOwnPropertyDescriptor(target, propertyKey) Object.getOwnPropertyDescriptor() 如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined
Reflect.getPrototypeOf(target) Object.getOwnPropertyDescriptor()
Reflect.has(target, propertyKey) in 运算符 判断一个对象是否存在某个属性
Reflect.isExtensible(target) Object.isExtensible()
Reflect.ownKeys(target) Object.keys() 返回一个包含所有自身属性(不包含继承属性)的数组
Reflect.preventExtensions(target) Object.preventExtensions() 返回一个Boolean
Reflect.set(target, propertyKey, value[, receiver]) 将值分配给属性的函数。返回一个Boolean如果更新成功则返回true
Reflect.setPrototypeOf(target, prototype) 设置对象原型的函数. 返回一个 Boolean 如果更新成功则返回true

Reflect静态方法

典型应用

const user = {
  name: 'Cxx',
  age: 18
}

const proxy = new Proxy(user, {
  get(target, property, receiver) {
    // 跟踪属性访问
    console.log(`get ${property}`)
    return Reflect.get(...arguments)
  },
  set(target, property, value, receiver) {
    // 跟踪属性访问
    console.log(`set ${property}`)
    return Reflect.set(...arguments)
  }
})

proxy.name
proxy.age = 19
console.log('**********')

const hiddenProperties = ['name']
const proxy2 = new Proxy(user, {
  get(target, property, receiver) {
    // 隐藏属性
    if (hiddenProperties.includes(property)) {
      return undefined
    } else {
      return Reflect.get(...arguments)
    }
  },
  has(target, property) {
    // 隐藏属性
    if (hiddenProperties.includes(property)) {
      return undefined
    } else {
      return Reflect.has(...arguments)
    }
  }
})

console.log(proxy2.name)
console.log('name' in proxy2)
console.log('**********')

const proxy3 = new Proxy(user, {
  set(target, property, value, receiver) {
    // 属性验证
    if (property === 'age' && typeof value !== 'number') {
      return false
    } else {
      return Reflect.set(...arguments)
    }
  }
})

proxy3.age = '20'
// 更改无效
console.log(proxy3.age)
proxy3.age = 20
console.log(proxy3.age)
console.log('**********')


// 输出
// get name
// set age
// **********
// undefined
// false
// **********
// 19
// 20
// **********

Promise与异步函数

JavaScript是一种单线程时间循环模型。

Promise基础

Promise(期约或者承诺)是ES6新增的引用类型,可以通过new操作符进行实例化。创建Promise时需要传入执行器函数作为参数该函数接收2个可以改变Promise状态的函数,一般命名为resolvereject

let p1 = new Promise((resolve, reject) => {
  console.log('p1 executor')
  resolve('p1')
})

let p2 = new Promise((resolve, reject) => {
  setTimeout(reject, 1000, 'p2 executor')
})

let p3 = new Promise(() => {

})

// p1 的状态为 fulfilled
console.log('p1:', p1)

// 此时 p2 的状态为 pending
console.log('p2:', p2)

// p3 的状态为 pending
console.log('p3:', p3)

setTimeout(() => {
  // p1 的状态为 fulfilled
  console.log('p1:', p1)

  // 此时 p2 的状态为 reject
  console.log('p2:', p2)

  // p3 的状态为 pending
  console.log('p3:', p3)
}, 1000);

Promise一共有3种状态待定pending、兑现fulfilled(或者称为解决resolved)和拒绝rejected。无论何时,Promise有且只可能是其中一种状态,且不可逆。待定是Promise的最初始状态,也是默认状态,当在Promise内部调用控制Promise状态的函数时,则会进行相应的状态切换,否则一致保持待定状态。
promise基础
  在p1中,立即调用resolve()函数将其状态转为解决,而在p2通过延迟1秒再将其状态转为拒绝p3会一直保持默认状态。
  因此,p1的状态会一直保持解决状态,并带有解决的返回值。p2的状态一开始为待定1秒后会变成拒绝状态也带有拒绝的返回值并且控制台会提示错误信息。p3则会一直处于待定状态,并没有返回值。

Promise实例方法

let p1 = new Promise((resolve, reject) => {
  console.log('p1 executor')
  setTimeout(resolve, 1000)
})

// then 只会在 Promise 进入 解决或者拒绝 状态时执行
p1.then(() => new Promise((resolve, reject) => {
  console.log('p2 executor')
  setTimeout(resolve, 1000, 'p2 value')
})).catch(() => {
  console.log('p1 catch')
}).finally(() => {
  console.log('p1 finally')
// then 仍然返回 Promise 可以继续链式调用
// 将解决/拒绝的值作为参数进行向下传递 
}).then((value) => new Promise((resolve, reject) => {
  console.log(value)
  console.log('p3 executor')
  setTimeout(reject, 1000)
// 也可以将 catch 写在 then 中 即作为第二个参数 
})).then(() => {
  console.log('p3 resolve')
}, () => {
  console.log('p3 reject')
})


// 输出
// p1 executor
// p2 executor
// p1 finally
// p2 value
// p3 executor
// p3 reject

then()实例方法接收2个参数第一个为处理解决状态的函数第二个为处理拒绝状态的函数并且可以将解决/拒绝状态的返回值作为函数的参数进行传递。Promise状态只能为其中一个,因此也只能进入其中一个函数。
catch()实例方法实际为then()方法的语法糖,表示处理拒绝状态的函数。finally()实例方法表示无论转换为解决还是拒绝状态都是执行的函数。
  如果Promise状态一直为待定状态,则不会进入这些实例方法。

Promise连锁

let p = Promise.all([
  // p1 也可以直接调用实例方法创建 Promise
  Promise.resolve('p1 executor'),
  
  // p2
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('p2 executor')
    }, 1000);
  }),

  // p3
  Promise.reject('p3 executor')
])

// 此时 p 的状态为 pending
console.log(p)

// 此时 p 的状态为 rejected
setTimeout(console.log, 1000, p)

// 只会进入 catch 实例方法
p.then((resolve) => {
  console.log(resolve)
}).catch((reject) => {
  console.log(reject)
})

// 输出
// Promise {<pending>}
// p3 executor
// Promise {<rejected>: "p3 executor"}

Promise.all()接收一个可迭代对象,会依次处理每个Promise的状态。如果至少有一个Promise的状态为待定/拒绝,则合成的Promise的状态为待定/拒绝,且此时返回的拒绝值为第一个拒绝的值。如果所有的Promise的状态为解决,则合成的Promise的状态为解决,且返回的解决值为所有解决值的数组。

let p = Promise.race([
  // p1 
  Promise.resolve('p1 executor'),

  // p2 
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('p2 executor')
    }, 1000);
  }),

  // p3
  Promise.reject('p3 executor')
])

// 此时 p 的状态为 pending
console.log(p)

// 此时 p 的状态为 fulfilled
setTimeout(console.log, 1000, p)

// 只会进入 then 方法
p.then((resolve) => {
  console.log(resolve)
}).catch((reject) => {
  console.log(reject)
})

Promise.race()Promise.all()类似,但只会处理最先解决/拒绝的Promise。只要有一个Promise已经解决/处理,则不会再进行后续操作。

异步函数

async关键字用于声明异步函数,异步函数始终返回期约对象,await关键字会暂停执行异步函数后面的代码,让出JavaScript运行时的执行线程,await关键字必须在异步函数中使用。

async function foo() {
  console.log('foo start')
  // 碰到 await 会暂停执行 直到 await 右边的状态确定
  const result = await Promise.resolve('p1')
  console.log('result:', result)
  console.log('foo end')
}

function bar() {
  setTimeout(() => {
    console.log('bar setTimeout')
  }, 1000);
  console.log('bar start')
}

foo()
bar()

// 输出
// foo start
// bar start
// result: p1
// foo end
// bar setTimeout

JavaScript运行时在碰到await关键字时,会记录在哪里暂停执行,等到await右边的值可用了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。

参考资料

  1. JavaScript高级程序设计 第四版
  2. ES6标准入门 阮一峰