1. 对象
对象创建
创建对象的两种方法:
- 构造函数:
let user = new Object();
- 字面量:
let user = {};
对象是属性可以随意添加:
- 点号:
user.name="Mason"
- 方括号:
user["age"]=26
对象字面量中可以用 [变量]
来表示属性名,叫做 计算属性:
let fruit = prompt("which fruit? ", "apple");
let bag = {
[fruit]: 5, // 属性名从 fruit 变量中动态得到
}
alert(bag.apple);
属性简写:
function makeUser(name, age) {
return {
name,
age,
}
}
// 相当于
function makeUser(name, age) {
return {
name: name,
age: age,
}
}
属性存在性测试:in
尝试访问 JavaScript 对象的不存在的属性时,不会报错,只是得到一个 undefined
值。使用 in
操作符可以检查对象中是否存在指定属性:"name" in user
。
对象引用与复制
引用复制:对象名被赋值给另一个变量时,复制的只是 对象的地址。
浅拷贝:Object.assign(dest, src1, src2)
,该方法将若干个源对象中的每一个属性复制到目标对象中,但如果某个属性的值是一个嵌套对象,那么只会复制这个对象的引用。
深拷贝:使用 for attr in obj
循环,遍历每一个属性。
对象方法
可以给对象绑定一个函数作为属性:
let user = { name: "Rachael" }
user.sayHi = function() {
console.log("Hi Monica.");
}
还可以在声明对象的时候绑定方法,这时可以用简写形式:
let user = {
sayHi() { console.log("hello"); }
}
对象方法中的 this
指向该方法所在对象。
可选链 ?.
如果访问一个对象不存在的属性,会返回 undefined
,但是如果访问这个不存在属性的属性,那就回出现错误,因为不能试图从 undefined
类型上获取属性。这种情况下,如果属性链又比较长的话,如果 对一个个属性手动检查是否存在 就太麻烦了,所以可选链 ?.
被引入了。
如果可选链前面的部分是 undefined
或 null
,就会停止后面的运算并返回前面这部分的值。
例如 value?.prop
:
- 如果
value
存在(不为null
且不为undefined
),则尝试正常访问其prop
属性; - 否则,返回
undefined
,而不是出现错误。
所以下面就是一种使用可选链安全访问 user.address.street
的方式:
console.log( user?.address?.street );
除了用于安全访问对象属性,可选链还可以用来 安全地调用函数:?.()
:
user.sayHi?.();
另外如果想要用方括号语法 访问属性,还可以用 ?.[]
:
console.log(user1?.["firstName"]);
Symbol
对象的属性只能是字符串类型或者 Symbol 类型。
Symbol 值表示唯一的标识符,使用 Symbol()
函数来创建,可以在创建时给函数传入一个字符串作为该 Symbol 值的描述(名字),名字不影响 Symbol 的唯一性。
用途之一——给对象添加隐藏属性:
Symbol 作为对象的属性名时,只能在当前文件访问到这个属性,在外部访问不到这个 Symbol:
let user = { name: 'Mason' }; // 外部对象
let id = Symbol("id");
user[id] = 1; // 只能用方括号语法来定义这个属性,因为属性名引用的是一个变量
console.log(user[id]);
Symbol 类型的值作为对象的属性名时,会被属性的 for...in...
循环忽略,但是在使用 Object.assign()
拷贝对象属性时,会被访问到。
全局 Symbol
常规情况下,即使多个 Symbol 有相同的名字,它们也是不同的值,但是如果希望维护一个全局的 Symbol,在不同地方都可以 通过同样的名字访问,且确保访问到的是同一个 Symbol,就可以使用 全局 Symbol 注册表。
// 从全局注册表中读取,不存在会新建
let id = Symbol.for("id");
let idAgain = Symbol.for("id");
console.log(id === idAgain); // true
2. 数据类型
数字
进制转换:nums.toString(base)
返回给定进制下 num
的字符串表示形式。
舍入:Math.floor
、Math.ceil
、Math.round
(四舍五入)。
保留指定位数小数:num.toFixed(2)
将 num
保留 2 位小数后以字符串形式返回,然后将字符串转为数字即可。
字符串转为数字:
Number(str)
+str
(一元加号)parseInt(),parseFloat()
:一元加号+
和Number()
函数需要被转换的字符串除了开头结尾空格之外,所有字符都是数字。而parseInt()
和parseFloat()
可以删除开头空格之后从头开始解析,解析出开头连续的数字,遇到第一个非数字字符后返回解析结果。如果是a12
这种以非数字开头的,会返回NaN
。
isNaN()
它会首先尝试将参数转换为数字,然后检查转换结果是否为数字:
isNaN("a12"); // true
isNaN("12"); // false
之所以有这个函数,是因为 NaN
这个值跟任何值都不相等,包括自身。
Math
对象
Math.random()
返回一个[0,1)
的随机数;Math.max(a,b,c,...)
,Math.min(a,b,c,...)
最值Math.pow(n, power)
幂
字符串
三种引号:支持单引号、双引号、反引号,反引号允许通过 ${expression}
嵌入表达式,且允许多行字符串。
访问字符:
- 使用下标:
str[pos]
(现代) - 使用方法:
str.charAt(pos)
(以前)
如果没有找到字符,下标会返回
undefined
,方法会返回空串。
遍历字符串:for (left c of str)
字符串长度:length
属性。
字符串不可变:不能通过下标的方式修改字符串中某个位置的字符。
子串相关方法:
- 查找子串:
str.indexOf(substr)
,找不到会返回-1
;includes(sub)
,startsWith(sub)/endsWith(sub)
。 - 获取子串:
slice(start, end)
,获取[start, end)
区间的子串。substring(s,e)/substr(s,e)
作用和slice(s,e)
相同,但主要使用slice()
方法。
比较字符串:
str.codePointAt(pos)
:返回指定位置字符的 ASCII 码String.fromCodePoint(code)
:返回指定 ASCII 码对应的字符str1.localeCompare(str2)
:表示两字符的字典序关系,如果str1
在str2
之前,则返回负数。
数组
数组长度:length
属性,可以手动修改这个属性,如果减少会导致数组被截断,所以 清空数组 的一个方法就是将 length
属性设为 0
。
元素类型:一个数组中的元素可以是各种类型的。
遍历
- 常规
for i
循环 for...of...
循环:for (let name of names)
forEach
循环:arr.forEach(function(item, index, array)) { ... }
常用 API:
-
删除/插入元素:
splice(start, [deleteCount, newElem1, newElem2, ...])
:删除从start
位置开始的deleteCount
个元素,并在删除位置插入新元素。将deleteCount
设置为0
,就可以不删除只插入。 -
slice(start, end)
:获取一个切片,左闭右开,索引支持负数。如果不指定start
,会从第一个元素开始切分;如果不指定end
,会切分到最后一个元素。 -
concat(arg1, arg2, ...)
:参数可以是单个的值,也可以是一个数组,将所有参数 拼接到原数组 之后返回。 -
查找元素:
indexOf(item, [start])
:找不到则返回-1
;includes(item, [start])
:判断item
是否存在。inlcudes()
可以正确处理NaN
,而indexOf()
不能。 -
搜索满足指定条件的元素:
find(item => item.name == "Rachael")
,findIndex(item => item.name == "Rachael")
—— 返回满足条件的 第一个 元素、元素的索引。找不到则返回undefined/-1
。filter(user => user.age > 18)
—— 返回满足条件的 所有 元素,返回一个数组。
-
排序:
sort()
原地排序,可以传入一个函数表示排序依据:sort((a,b) => a-b )
—— 表示从小到大排序。reverse()
原地逆序
-
分割:
str.split(sep)
将一个字符串根据指定分隔符(默认为空字符)分割为 字符数组;arr.join(sep)
刚好相反,将一个数组中的各个元素根据指定分隔符连成字符串。 -
元素映射:
arr.map(function(item, index, array) { ... })
:在每个元素上调用一遍函数。 -
迭代:
reduce/reduceRight
let value = arr.reduce(function(accumulator, item, index, array) { //... }, [initial]); // initial 是迭代结果的初值
例如:
let arr = [1,2,3,4]; let sum = arr.reduce((sum, item) => sum + item, 0);
-
Array.from()
将一个 array-like 对象转换为数组。所谓 array-like 对象,就是有索引和 length 属性的对象。
Map
Map 和 JavaScript 对象在形式上很像,但是 Map 允许任何类型的键,还提供了一系列方法。
let map = new Map();
map.set('1', 'str1');
map.set(1, 'num1');
map.size // 2
map.has(1) // true
map.delete(1)
甚至也可以将 JavaScript 对象作为 Map 的键。
set()
方法返回当前对象,所以可以链式调用。
遍历
三个方法:keys(), values(), entries()
。
for (let k of map.keys())
console.log(map.get(k));
也可以用 forEach()
:
map.forEach((value, key, map) => {
// ... 注意第一个参数是 value,第二个才是 key
})
对象和 Map 的转化
- JavaScript 对象 → Map:
Object.entries(obj)
方法可以将 JavaScript 对象转换为 二维的键值对数组 :[[key1, value1], [key2, value2], ...]
,将这样的数组传入Map()
构造函数就可以构造出一个 Map。 - Map → JavaScript 对象:
Object.fromEntries()
方法相反,负责根据给定的二维键值对数组创建 JavaScript 对象:Object.fromEntries(map.entries())
。
Set
let set = new Set();
let rachael = { name: "Rachael" }
let monica = { name: "Monica" }
set.add(rachael)
set.add(monica)
set.size // 2
// for of 循环
for (let friend of set) console.log(friend.name)
// forEach 循环
set.forEach((value) => console.log(value))
解构赋值
数组解构
let arr = ["Chandler", "Bing"]
let [first, last] = arr
或者:
let [first, last] = "Mason Li".split(" ")
可以 使用逗号忽略一个位置的值:
let [name, , age] = ["Chandler", "Bing", 27]
等号右侧可以是任何 可迭代对象。
用途 1:通过解构 遍历对象的键值对:
for (let [key, value] of Object.entries(user)) { ... }
用途 2:交换多个变量的值:
[val1, val2] = [val2, val1]
用途 3:使用 rest 参数(...arg
)可以接收剩余的所有参数:
// rest 会成为一个数组
let [name1, name2, ...rest] = ["Rachael", "Chandler", "Monica", "Febbe"]
对象解构
根据 属性名 可以从对象中解构出对应属性:
- 属性名的顺序不重要
- 可以给变量设置 默认值。
let options = {
title: "Menu",
width: 100,
height: 200
}
let {width=150, height=250, title} = options;
还可以把一个属性赋值给另一个名字的变量:
let {width: w = 150, height: h = 250} = options
rest
参数:
let {title, ...rest} = options
不使用
let
而直接用花括号进行解构时,会出错:let title, width, height; {title, width, height} = options // 出错
这是因为 JavaScript 会把花括号中的内容当做一个代码块。
为了告诉 JavaScript 这不是一个代码块,可以把整个赋值表达式用圆括号包裹起来:
({title, width} = options)
嵌套解构
嵌套解构会 解构到最内层,外层的变量不会被赋值。
let options = {
item: ["Cake", "Donut"],
size: {
width: 200,
height: 100
}
};
let {
size: {
width,
height
},
item: [item1, item2]
} = options;
item // Error
size // Error
item1 // "Cake"
width 200
用对象作为函数参数
function showMenu({title = "untitled", width = 200, height = 100}) {
// ...
}
let options = {
title: "My Menu",
items: ["item1", "item2"]
};
showMenu(options);
JSON
将 JavaScript 对象转换为 JSON 字符串 —— JSON.stringify()
JSON 对象与 JavaScript 对象有两个区别:
- JSON 对象没有单引号或反引号,只用双引号
- JSON 对象的 属性名也要用双引号
转换时会跳过一些特定于 JavaScript 对象的属性:
- 方法
- Symbol 类型属性
- 值为
undefined
的属性
将 JSON 字符串转换为 JavaScript 对象 —— JSON.parse()
3. 函数
函数没有返回值时,默认返回
undefined
。
Rest 参数与 Spread 语法
Rest 参数
调用函数时传入参数多于函数定义时参数个数也不会出错,但是多余的参数会被忽略。使用 Rest 参数可以 将剩余参数收集到一个数组中。
function getGrade(name, ...grades) {
console.log(name)
for (let grade of grades)
console.log(grade)
}
Rest 参数必须放到参数列表末尾。
Spread 语法
Spread 语法形式上和 rest 参数很像,也是使用三个点 ...
,但是作用刚好相反,它是 将一个可迭代对象中的所有元素展开成参数列表。
let arr = [1,3,5];
let max = Math.max(...arr); // 展开式语法
还可以同时 传递多个可迭代对象,并能同时与常规参数结合使用:
let max = Math.max(0, ...arr1, ...arr2, 7);
除了函数调用,Spread 语法的其他用法:
-
合并数组:
let merge = [...arr1, 10, ...arr2, 20];
-
复制数组:
let arrCopy = [...arr];
-
复制对象:
let objCopy = {...obj};
使用 Spread 语法复制数组或对象比
Object.assign()
方法简洁很多,所以这种方式更为常用。
装饰器模式
可以给函数传入一个函数作为参数。
例如有一个耗时操作 slow()
,为了提高它的速度,可以将它的执行结果缓存下来,再次调用时,如果对应的缓存结果存在,就直接返回这个缓存。
function slow(x) { ... }
function cacheSlow(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) return cache.get(x);
let result = func(x); // 没有缓存再调用
cache.set(x, result);
return result;
};
}
这里再函数内返回了一个函数,这种语法现象叫做 闭包,所谓闭包,就是 携带状态的函数,或者说,闭包是 一个函数及其绑定的周边环境的组合。在执行完 cacheSlow()
函数之后,它内部的局部变量 cache
不会被回收,因为它还被内部的函数引用。(所以这个变量被分配到堆区)
call/apply
call()/apply()
这两个高阶函数可以 给其他函数绑定 this
:
func.call(context, arg1, arg1)
function sayHi() { console.log("Hi " + this.name); }
let friend = { name: "Rachael" };
sayHi.call(friend); // "Hi Rachael"
apply()
和 call()
唯一的不同在于调用方式,call
接收一个参数列表,而 apply
接收一个包含这些参数的 类数组对象:
func.call(context, ...args) // args 是一个类数组对象,将其展开为参数列表
func.apply(context, args) // 直接传入对象
绑定 this
一旦方法被传递到 与对象分开 的某个地方,this
就会丢失。
let user = {
name: "Mason",
sayHi() { console.log(`Hello, ${this.name}`); }
};
setTimeout(user.sayHi, 1000); // sayHi 方法与 user 对象分开了,this 变成了全局对象
使用 bind()
方法可以给一个函数绑定任意 this
:
let friend = { name: "Rachael" };
function getName() { console.log(this.name); }
let getNameBinded = getName.bind(friend);
getNameBinded(); // "Rachael"
箭头函数
箭头函数没有 this,如果访问 this,它会从外部获取。
let group = {
groupId: 1,
names: ["Mason", "John", "Mike"],
showList() {
this.names.forEach(
name => console.log(this.groupId + name)
); // 能够正确获取 this.groupId
}
};
group.showList(); // 正确运行
因为没有自己的 this,所以上面的箭头函数会到外部(即 group
对象)获取 this
。而普通函数是有 this
的,默认为 undefined
。
因为不具有
this
,所以也就不能将箭头函数用作构造器,不能用new
调用它。
4. 原型和继承
__proto__
属性
对象有一个隐藏属性 [[Prototype]]
,指向其原型(父类型),如果没有原型则为 null
。
从对象中读取一个不存在的属性时,会自动往原型中查找这个属性,这就是“原型继承”行为。
访问/修改 [[Prototype]]
属性的方式为
__proto__
属性。__proto__
属性是[[Prototype]]
属性的getter/setter
,这是历史遗留问题,已经不推荐使用。- 推荐使用
Object.getPrototypeOf()
、Object.setPrototypeOf()
方法。
属性的迭代
使用 for in
循环迭代对象属性时会包含继承自原型的属性,如果不想考虑这些属性,可以通过 obj.hasOwnProperty(prop)
判断条件来过滤,如果 prop
是从原型继承来的,会返回 false
。
prototype
属性
JavaScript 的所有 函数 都有 prototype
属性。
JavaScript 对象分为 函数对象 和 普通对象,每个对象都有
__ptoto__
属性,但只有函数对象才有prototype
属性。
构造函数、实例、原型三者的关系
(构造)函数 的 prototype
属性指向了一个对象,这个对象就是那些被该函数构造出来的所有 实例对象 的 原型(或者说父对象,__proto__
所指对象),每个对象都能从原型对象继承属性。
实例的
__proto__
指向构造函数的ptototype
对象。
每个原型都有一个 constructor
属性,指向关联的 构造函数。
function Person() {} // 构造函数
let person = new Person(); // 实例对象
person.__proto__ === Person.prototype // true
Person.prototype.constructor === Person // true
原型链
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
那么,原型的原型又是什么呢?
原型也是一个 对象,是通过 Object()
构造函数 生成的,那么原型就是这个构造函数的实例,那么它的原型就指向 Object.prototype
对象。
Object.prototype
对象是顶层对象,它的原型被设计为 null
。
5. Class
语法
class User {
// 构造函数:在这里定义类的数据域
constructor(name) { this.name = name; }
// 方法
sayHi() { console.log("Hi " + this.name); }
}
let user = new User("Rachael");
user.sayHi(); // "Hi Rachael"
类属于函数
typeof User // function
声明类 class User { ... }
的时候,实际上做了下面两件事:
- 创建名为
User
的函数,函数体来自constructor
方法。 - 在
User
原型中存储类中声明的方法:User.prototype.sayHi
等。
但类不仅仅是语法糖
类和函数存在很大差异:
- 在类中创建的函数具有属性
[[IsClassConstructor]]:true
; - 类方法不可枚举,所有方法的
enumerable
属性都为false
; - 类的其他功能。
getter/setter
class User {
constructor(name) {
this.name = name; // 调用 setter
}
get name() { // 用 get 关键字指明该方法为 getter
return this._name;
}
set name(value) { // 用 set 关键之指明该方法为 setter
this._name = value;
}
}
let user = new User("Mary");
user.name // Mary
user.name = "Jenifer";
user.name // Jenifer
定义 getter/setter 之后,还是直接用属性名访问属性(user.name
),只是在 getter/setter
内部可以定义一些附加的逻辑。
继承
通过 extends
关键字建立原型链:
class Dog extends Animal { ... }
extends
将 Dog.prototype.__proto__
设置为了 Animal.prototype
对象,还将 Dog.__proto__
设置为了 Animal
。
重写父类(原型)中的方法
super
关键字用来引用父类:
super.method()
调用父类的方法;super(...)
调用父类构造函数。
箭头函数不能使用
super
。
子类的构造函数必须调用 super()
,且必须在使用 this
之前就调用。
静态成员
可以把一个方法赋值给类本身,而不是它的 prototype
对象,这样的方法是 static
方法。同样地,可以设置静态属性。
class User {
static staticMethod() { ... }
static staticField = "Hello";
}
User.staticMethod(); // 通过类直接调用
私有属性
受保护属性
受保护属性通常以下划线作为前缀,提醒开发者,这个属性不要从外部访问。但这只是一种约定,变量名加不加下划线实际上没有区别。
只读
要让某个属性只读,可以只给它设置 getter,而不设置 setter。这样从外部修改这个属性时就会报错。
私有属性
上面的受保护属性毕竟不受语言支持,所以最新的提案为私有属性和方法提供了语言级的支持:私有属性和方法以 #
符号开头,只能在类的内部被访问。这个提案即将/已经加入到最新的规范。
6. 异步
Promise
执行任务,返回 Promise 对象
let promise = new Promise(function(resolve, reject) {
// ... 执行任务
})
在任务执行完毕时,可以调用 resolve(value)
、reject(error)
这两个函数之一,这是两个 JavaScript 提供的函数。
- 如果正确完成了任务,就调用
resolve(value)
,这会将构造出的 Promise 对象的state
属性变成fulfilled
,将result
属性变成value
。 - 如果出现了错误,就调用
reject(error)
,这会将构造出的 Promise 对象的state
属性变成rejected
,将result
属性变成error
。
Promise 对象的 state
和 result
属性可以在后续的 .then()
方法中引用,来异步处理执行结果。
then、catch、finally 处理执行结果
.then()
可以接收两个回调函数作为参数,分别处理 result
和 error
。一条最佳实践是,在 .then()
中只处理 result
,error
放到 .catch()
中来处理。
在 .then()
中返回的值可以传入下一个 .then()
,所以 .then()
可以链式调用。
Promise API
1️⃣ Promise.all([promise1, ...])
这个方法用得最多,接收 Promise 对象数组 作为参数,等 所有 Promise 对象都成功执行 后才返回一个结果,这个结果也是一个 Promise 对象,其 value
属性为各个 Promise 结果组成的数组。
一旦有一个 Promise reject 了,Promise.all
就会立即 reject,并将这个错误返回。
例子:
let names = ["Rachael", "Monica"]
let url = "https://friends.com/friend/"
let requests = names.map(name => fetch(`${url}${name}`))
Promise.all(request).then(...)
2️⃣ Promise.allSettled
和 Promise.all
类似,但它会等所有 Promise 对象都变成 settled
状态时才返回各个 Promise 的结果。(resolve 或者 reject 之后都会变成 settled 状态)
3️⃣ Promise.race
等任何一个 Promise 对象变成 settled 状态时就结束。
async/await
这两个关键字是 Promise 的语法糖。
async 函数
async
放在函数前面,确保函数的返回值是一个 Promise 对象,如果返回普通值,会被包装成一个 Promise 对象。
async function f() { return 1; }
f().then(console.log); // 1
await 关键字
await
关键字只能用在 async 函数中,它让 JavaScript 执行引擎等待,直到 Promise 执行完成。
async function f() {
let promise = new Promise(
resolve => setTimeout(() => resolve("done"), 1000)
);
let result = await promise; // 主线程暂停执行,直到 promise 返回
console.log(result); // 此时 result 已经有了期望的值
}
await
是用来代替 then
的,它以同步的方式书写异步代码。
捕获错误
与 then
搭配使用的是 catch
,而在 async/await
中,通常直接使用 try...catch
。
async function f() {
try {
let response = await fetch(url);
} catch(err) {
console.log(err);
}
}
7. 模块
导出的语法:
1️⃣ 在声明前导出
可以在声明之前放置 export
来标记任意声明为导出:
export let names = ["Rachael", "Monica"];
export function f() { ... }
2️⃣ 导出与声明分开
先声明变量、函数等,然后在最后导出:
function foo() { ... }
function bar() { ... }
export {foo, bar}; // 导出变量列表
3️⃣ 默认导出
实际开发中主要有两种模块:
- 包含 函数包 的模块
- 声明 单个实体 的模块
大部分情况下,第二种方式更为推荐,这样可以让模块的组织更加规范。为此模块提供了一个特殊的 默认导出 语法,以便支持 一个模块只做一件事 这个原则。
export default class User {
...
}
一个文件 只能有一个默认导出,导入这种被默认导出的内容时,不需要写花括号:
import User from './user.js';
还有几点需要注意: