JavaScript简介
JavaScript 实现
完整的JavaScript
实现包含以下几个部分:
- 核心(
ECMAScript
) - 文档对象模型(
DOM
) - 浏览器对象模型(
BOM
)
ECMAScript
只是一个语言规范的标准,而JavaScript
实现了这一标准。
文档对象模型DOM
(Document Object Model
)是一个应用编程接口,用于在HTML
中使用扩展的XML
。DOM
将整个页面抽象为一组分层节点。
浏览器对象模型用于支持访问和操作浏览器的窗口。
语言基础
变量
var let const
let
声明的范围是块作用域,var
声明的范围是函数作用域。块作用域是函数作用域的子集。
var
与let
的区别:
var
:函数作用域;存在变量提升;可重复定义;声明的变量会作为window
的属性。
let
:块级作用域;不存在变量提升(有暂时性死区);不可重复定义;声明的变量不会作为window
的属性。
块级作用域:即在{}花括号内的域,由{ }包括,比如if{}
块、for(){}
块。
函数作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体都是有定义的。
暂时性死区:在代码块中,在声明变量之前,该变量是不可用的。
const
的行为与let
基本相同,唯一的重要区别为声明变量的同时必须初始化变量。定义和声明变量时,
const
优先,let
次之。
数据类型
ECMAScript
变量包含两种类型的数据:原始值和引用值。
原始值就是简单的数据类型,引用值则是由多个值构成的对象。保存原始值的变量是按值访问的,实际操作的就是存储在变量中的实际值。而引用值是保存在内存中的对象,JavaScript
不允许直接访问内存位置,因此操作对象(引用值)时,实际操作的是对该对象的引用,因此保存引用值的变量是按引用访问的。
原始值
原始值一共有6种,即Undefined、Null、Boolean、Number、String和Symbol
。
引用类型
集合引用类型
Object
- 对象字面量表达式中,属性名可以是字符串或数值,数值属性会自动转换为字符串。
- 使用中括号可以通过变量访问属性,也可以在属性名中包括可能会导致语法错误的字符。
1 | let person = { |
Array
from()
用于将类数组结构转换为数组实例,of
用于将一组参数转换为数组实例。
1 | console.log(Array.from('Cxx')) |
如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值
+1
。通过修改length
属性,可以从数组末尾删除或添加元素。检测一个值是否为数组:
Array.isArray()
。keys()
返回数组索引的迭代器,values()
返回数组元素的迭代器,entries()
返回索引/值对的迭代器。
1 | const a = ['foo', 'bar'] |
数组方法
基本方法
- 栈方法:
1 | // 接收任意数量的参数,并将其添加至数组末尾,返回数组的最新长度 |
- 队列方法:
1 | // 删除数组的第一项并返回它,然后数组长度-1 |
- 排序方法:
1 | // 将数组元素反向排列 |
操作方法
- concat()
1 | // 将其参数添加至副本末尾 然后返回这个新数组 |
- slice():切片
1 | // 参数:返回元素的开始索引和结束索引 返回值:对应索引的元素 |
- splice():拼接
1 | // 参数:索引值 需要删除的个数(可以为0) 需要插入的元素(可以为0) |
搜索方法
indexOf()
、lastIndexOf()
和includes()
1
2
3
4
5
6
7
8
9// 参数:要查找的元素,搜索位置 返回值:查找元素在数组的位置
// 从第一项向后搜索
index = indexOf(searchItem, [searchInedex])
// 从最后一项向前搜索
index = lastIndexOf(searchItem, [searchInedex])
// 参数:同上 返回值:是否找到一个与指定元素匹配的项
isInclude = includes(searchItem, [searchInedex])
自定义搜索:find()
和findIndex()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 参数:断言函数(元素、索引和数组本身) 返回值:匹配项
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
1
2
3
4
5
6
7
8let 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
// > truefilter():返回每次函数调用的结果为
true
的项组成的数组- map():返回每次函数调用的结果组成的数组
1 | let numbers = [1, 2, 3, 4, 5] |
- forEach():循环遍历,不返回结果
1
2
3
4
5
6
7
8
9
10
11
12let 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
使用new
和Map
构造函数就可以创建一个空映射,也可以给Map
构造函数传入一个可迭代对象进行实例化。
Map
可以使用任何JavaScript
数据类型作为键。
映射实例可以提供一个迭代器,并且会维护键值对的插入顺序。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// 创建空映射
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
1 | // 创建空映射 |
WeakSet
- 成员只能是
Object
或者继承自Object
的类型。 - 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存
DOM
节点,不容易造成内存泄漏。 - 不能遍历。
总结
Map
是键-值对,Set
是值-值对。
迭代器与生成器
迭代器
迭代器概念
可迭代对象:实现了正式的Iterable
接口,而且可以通过迭代器Iterator
消费。
实现Iterable
接口:需要同时具备支持迭代的自我识别能力和创建实现Iterator
接口的对象的能力。(也即必须暴露一个Symbol.iterator
属性作为默认迭代器)
迭代器:是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现Iterable
接口的对象都有一个Symbol.iterator
属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也是一个函数,调用之后就会产生一个实现Iterator
接口的对象。
迭代器是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API
。
实现Symbol.iterator
方法的对象就是可迭代对象,该方法返回一个迭代器。实现了next
方法的对象就是迭代器。可迭代对象和迭代器可以合并为一个对象,即同时实现Symbol.iterator
和next
方法。
可迭代对象是按一定规则建立好的对象(函数或者其他),迭代器则是需要的时候实例化即可,每个迭代器都拥有一定的API
。
迭代器使用
接收可迭代对象的原生语言特性:
1.for-of
循环 2.数组解构 3.扩展操作符 4.Array.from()
5.创建集合 6.创建映射 7.yield*
操作符,在生成器中使用。
迭代器API
使用next()
在可迭代对象中遍历数据,返回done(true/false)
和value
值。
只要迭代器到达done:true
状态,后续调用next()
就一直返回同样的值。不同迭代器的实例相互之间没有联系,只会独立遍历可迭代对象。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化。
原生可迭代对象类型的迭代器
1 | let arr = ['foo', 'bar'] |
实现Iterable
接口的内置类型:
1.字符串 2.数组 3.映射 4.集合 5.arguments
对象 6.NodeList
等DOM
集合类型
自定义迭代器
在函数/对象中实现next()
接口即可构成迭代器,但此时只能调用迭代器API
,无法使用可迭代对象的特性,如for-of
循环。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43function 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
构造,也可以使用对象字面量的形式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37class 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
变量,使得可以同时创建多个迭代器。
1 | let makeRangeIterator = { |
生成器
生成器概念
在函数名称前面加上星号(*)
表示它是一个生成器(箭头函数不能用来定义生成器函数),调用生成器函数会产生一个生成器对象。
生成器对象内部实现了Iterator
接口,一开始处于暂停执行状态,调用next()
方法可以让生成器开始或恢复执行。
next()
方法中的value
属性是生成器函数的返回值,生成器函数只会在初次调用next()
方法后开始执行。
使用生成器可以快速的构造迭代器,只需要在内部编写逻辑即可:
例如将之前的示例改为使用生成器构造:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function* 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
关键字必须只能位于生成器函数中定义。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function* 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}
生成器应用
1 | // 生成器对象作为可迭代对象 |
总结
实现[Symbol.iterator]
属性的对象就是可迭代对象,可迭代对象就可以使用for-of
等原生的语言特性,实现next()
方法的就是迭代器,就可以使用相应的API
。在函数名称前加上*
就是生成器,生成器内部实现了迭代器的接口,因此也可以使用迭代器的API
,但同时也具有yield
等特殊特性。
创建迭代器的方法有以下几种:
使用原生具体可迭代对象性质的数据类型,如:
1
2
3const str = 'abc'
const strIterator = str[Symbol.iterator]()
console.log(strIterator.next())自定义具有
[Symbol.iterator]
属性和next()
方法的对象/函数。使用生成器
本质上相对于可以自定义数据类型,创建具有类似于数组功能的数据结构,使用更改灵活。
参考资料
对象、类
对象为一组属性的无序集合,可以将其想象为一张散列表,其中内容就是一组名/值对,值可以是数据或者函数。
对象属性
为对象的数据添加一些额外的属性效果,可以使用Object.defineProperty(Object, keyName, options)
定义,也可以同时定义多个(使用Object.definePropertys(Object, options)
)。
使用Object.getOwnPropertyDescriptor(Object, keyName)
可以查看指定属性的描述符,也可以使用Object.getOwnPropertyDescriptors(Object)
查看所有的属性描述符。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// 定义对象,包含一个直接定义在对象上的属性
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)
输出结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25{
"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增强的对象语法
1 | let name = 'cxx' |
创建对象
Object构造函数
1 | let person = new Object() |
对象字面量
1 | let person = { |
以上两种方式创建对象比较方便,但是无法创建具有同样接口的多个对象。
工厂模式
1 | function createPerson(name, age, job) { |
可以解决创建多个类似对象的问题,但是无法确定新创建对象的类型。
构造函数模式
1 | function Person(name, age, job) { |
这里使用了构造函数(Person()
)来代替之前的工厂函数。
同时使用new
操作符来创建Person
的实例,以这种方式创建时,会执行以下步骤:
- 在内存重创建一个新实例对象。(即
person1
和person2
) - 在新实例对象内部的
[[Prototype]]
(也即__proto__
属性)特性被赋值为构造函数的prototype
属性。(即函数的prototype
属性等于实例对象的__proto__
属性:Person.prototype === person1.__proto__
) - 构造函数内部的
this
被赋值为这个新的实例对象。(this
指向新对象,即运行时赋值,this.name
即新实例对象的name
) - 执行构造函数内部的代码。(给新的实例对象添加属性和方法)
- 如果构造函数返回非空对象,则返回改对象,否则,返回刚创建的对象。
这里需要记住2点:①__proto__
和constructor
属性是对象所独有的;②prototype
属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__
和constructor
属性。
constructor
属性是用来标识对象类型的,始终指向创建当前对象的构造函数。
任何函数只要使用new
操作符调用就是构造函数。
然而使用构造函数创建对象仍然存在问题,因为在JavaScript
中,函数也是对象。因此在创建sayName()
函数时,本质上相当于:1
2
3
4
5
6
7
8
9
10
11
12
13
14function 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
属性,该属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法,这个对象也是通过调用构造函数创建的对象的原型。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function 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
所有的属性和方法都直接添加到Person
的prototype
属性上了,使用这种原型模式定义的属性和方法是由所有的实例对象共享的。
深入理解原型
在JavaScript
中,只要创建了一个函数,就会按照特定的规则为这个函数创建一个prototype
属性(指向原型对象)。所有的原型对象自动获得一个名为constructor
的属性,指回与之关联的构造函数。
在自定义构造函数时,原型对象默认只会获得constructor
属性,其他的所有方法都继承自Object
(本例中的其他方法是在后面自定义的)。每次调用构造函数会创建一个新实例对象,这个实例对象的内部[[Prototype]]
指针就会被赋值为构造函数的原型对象(大部分浏览器将这个指针定义为实例对象上的__proto__
属性)。通过这个属性就可以访问对象的原型。
因此,实例对象通过__proto__
可以链接到原型对象,它实际上指向隐藏特性[[Prototype]]
,构造函数通过Prototype
属性链接到原型对象。实例对象与构造函数原型之间有直接的联系,但实例对象与构造函数之间没有关系。
在通过对象访问属性时,如果没有找到这个属性,则搜索会沿着指针进入原型对象。只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性。
只要通过对象可以访问属性或方法,in
操作符就返回true
(例'name' in person1
),而hasOwnProperty()
只有属性存在于实例上才返回true
。
附:参考资料:
对象迭代
Object.values()
返回对象值的数组,Object.entries()
返回键值对的数组。
注意事项
1 | function Person() {} |
通过原型模式创建对象也存在问题:
- 弱化了向构造函数传递初始化参数的能力
- 所有实例对象可以共享原型对象的属性和方法
继承
构造函数、原型对象和实例对象之间的关系:每个构造函数都有一个原型对象,原型对象有一个属性指回构造函数,而实例有一个内部指针指向原型对象。
原型链:通过将原型对象赋值为另一个类型的实例。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function 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
新的基础性语法糖结构,本质上仍是原型和构造函数的概念。
基础知识
1 | // 表达式声明 |
类可以包括构造函数方法、实例方法、获取函数、设置函数和静态类方法。
constructor
关键字用于在类定义块内部创建类的构造函数,在使用new
操作符创建类的实例时,会调用该函数。
在JavaScript
中,类就是一种特殊的函数,因此类也有prototype
属性,该原型对象也有一个constructor
属性指向类本身。
类是JavaScript
的一等公民,也可以像其他对象或函数引用一样把类作为参数传递。
每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享。
继承
ES6
类支持单继承,使用extends
关键字。类和原型上定义的方法都会被带到派生类。
Super关键字
1 | class Vehicle { |
super
既可以当函数使用,也可以当对象使用。子类的构造函数必须执行一次super
函数。
不能单独引用super
关键字,不能在调用super()
之前引用this
。
函数
函数实际上也是对象,每个函数都是Function
类型(属于引用类型)的实例。Function
也有属性和方法。
JavaScript
引擎在任何代码执行前,会先读取函数的声明。
箭头函数
箭头函数不能使用arguments
、super
和new.target
,也不能用作构造函数,箭头函数也没有prototype
属性。
函数名
函数名就是指向函数的指针,一个函数可以拥有多个名称。使用不带括号的函数名会访问函数指针,而不会执行函数。
ES6
中所有的函数对象都会暴露一个只读的name
属性,包含函数的信息。
函数参数
1 | // 扩展运算符收集参数 |
在JavaScript
中,函数参数只是为了方便写出来,并不是必须写出来,并没有验证命名参数的机制,因此传递任意数量的参数都是可以的。
arguments
对象是一个类数组对象,存放着函数的参数信息。ES6
中可以显示定义默认参数。函数的默认参数只有在函数调用时才会求值,不会在函数定义时求值。
使用扩展运算符在收集参数时,只能把它作为最后一个参数。
函数内部
this
在标准函数中,this
引用的是把函数当成方法调用的上下文对象,在箭头函数中,this
引用的是定义箭头函数的上下文。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function 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()
方法时,需要将参数一一列出。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37window.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
函数作为值
可以把函数作为参数传给另一个函数,也可以在一个函数中返回另一个函数。(实际上就是把函数名当指针使用)
闭包
闭包指的是引用了另一个函数作用域变量的函数,通常是在嵌套函数中实现的。1
2
3
4
5
6
7
8
9
10
11
12// 正常函数
function compare(value1, value2) {
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
let result = compare(5, 10)
如图所示,compare()
函数是在全局上下文中调用的,第一次调用时,会为它创建一个包含arguments
、value1
和value2
的活动对象,该对象是其作用域链上的第一个对象,而全局上下文的变量对象则是compare()
作用域链上的第二个对象,包含this
、result
和compare
。
全局上下文会在代码执行期间始终存在,而函数局部上下文只在函数执行期间存在。
在定义compare()
函数时,会为其创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]
中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]
来创建其作用域链,接着会创建函数的活动对象并将其推入作用域链的前端。
函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中只剩下全局作用域。
然而,闭包中就不一样了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 闭包函数
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()
执行完毕后,匿名函数仍然有它的引用,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁。
闭包的作用
闭包常常用来间接访问一个变量,或者隐藏一个变量。(使用全局变量也可以满足,但是会导致变量变为公有,不能起到一定的保护作用。)
附:关于闭包的形式解释
这里有一个小故事,可以更好的理解函数闭包的含义:公主的故事
有一个公主…1
function princess() {
她生活在一个充满冒险的奇妙世界。她遇到了她的白马王子,骑着独角兽环游世界,与龙搏斗,遇到会说话的动物,以及许多其他奇幻事物。1
2
3
4
5
6
7
8
9var adventures = [];
function princeCharming() { /* ... */ }
var unicorn = { /* ... */ },
dragons = [ /* ... */ ],
squirrel = "Hello!";
/* ... */
但她总是不得不回到她枯燥的家务和成年人的世界。
1 | return { |
她经常告诉他们她作为公主的最新惊人冒险。1
2
3
4
5 story: function() {
return adventures[adventures.length - 1];
}
};
}
但他们看到的只是一个小女孩……1
var littleGirl = princess();
…讲述关于魔法和幻想的故事。1
littleGirl.story();
即使大人们知道真正的公主,他们也永远不会相信独角兽或龙,因为他们永远看不到它们。大人们说,他们只存在于小女孩的想象中。
但我们知道真相;那个带着公主的小女孩……
……真是个公主,里面有一个小女孩。
Princess()
函数是一个包含私有数据的复杂作用域。在函数之外,不能看到或访问私有数据。公主把独角兽、龙、冒险等都留在了她的想象中(私人数据),大人自己看不到。但是公主的想象力被story()
函数的闭包捕捉到了,这是littleGirl
实例暴露给魔法世界的唯一接口。
参考资料
代理与反射
代理用于定义基本操作的自定义行为,本质属于元编程(meta programming
),可以修改程序的默认形为,就形同于在编程语言层面上做修改。
元编程,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。
代理是目标对象的抽象,可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。
代理基础
1 | const target = { |
代理使用
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 操作符 |
1 | const target = { |
处理程序对象中所有可以捕获的方法都有对应的反射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 |
典型应用
1 | const user = { |
Promise与异步函数
JavaScript
是一种单线程时间循环模型。
Promise基础
Promise
(期约或者承诺)是ES6
新增的引用类型,可以通过new
操作符进行实例化。创建Promise
时需要传入执行器函数作为参数,该函数接收2个可以改变Promise
状态的函数,一般命名为resolve
和reject
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32let 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
状态的函数时,则会进行相应的状态切换,否则一致保持待定状态。
在p1
中,立即调用resolve()
函数将其状态转为解决,而在p2
中,通过延迟1秒再将其状态转为拒绝,而p3
会一直保持默认状态。
因此,p1
的状态会一直保持解决状态,并带有解决的返回值。p2
的状态一开始为待定,1秒后会变成拒绝状态,也带有拒绝的返回值,并且控制台会提示错误信息。p3
则会一直处于待定状态,并没有返回值。
Promise实例方法
1 | let p1 = new Promise((resolve, reject) => { |
then()
实例方法接收2个参数,第一个为处理解决状态的函数,第二个为处理拒绝状态的函数,并且可以将解决/拒绝状态的返回值作为函数的参数进行传递。Promise
状态只能为其中一个,因此也只能进入其中一个函数。
catch()
实例方法实际为then()
方法的语法糖,表示处理拒绝状态的函数。finally()
实例方法表示无论转换为解决还是拒绝状态都是执行的函数。
如果Promise
状态一直为待定状态,则不会进入这些实例方法。
Promise连锁
1 | let p = Promise.all([ |
Promise.all()
接收一个可迭代对象,会依次处理每个Promise
的状态。如果至少有一个Promise
的状态为待定/拒绝,则合成的Promise
的状态为待定/拒绝,且此时返回的拒绝值为第一个拒绝的值。如果所有的Promise
的状态为解决,则合成的Promise
的状态为解决,且返回的解决值为所有解决值的数组。
1 | let p = Promise.race([ |
Promise.race()
和Promise.all()
类似,但只会处理最先解决/拒绝的Promise
。只要有一个Promise
已经解决/处理,则不会再进行后续操作。
异步函数
async
关键字用于声明异步函数,异步函数始终返回期约对象,await
关键字会暂停执行异步函数后面的代码,让出JavaScript
运行时的执行线程,await
关键字必须在异步函数中使用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24async 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
运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
参考资料
- JavaScript高级程序设计 第四版
- ES6标准入门 阮一峰