JavaScript 基础部分⚓︎
约 9525 个字 602 行代码 预计阅读时间 55 分钟
基础知识⚓︎
- 引入 JavaScript 脚本的方式:
- 内部嵌入:使用
<script>
标签,里面直接写 JS 代码(仅适用于代码量少的情况,不推荐) - 外部导入:在
<script>
元素中使用src
属性,它的值为 JS 外部文件的地址(本地或 URL 均可) ,此时会忽略标签内的任何内容。推荐用这种方法,因为浏览器会下载 JS 文件并保存到缓存中,下次打开网页无需再次下载,使页面加载更快
- 内部嵌入:使用
- 注释:同 C 语言
- 在 JS 代码开头添上
"use strict";
,JS 将启用严格模式,使用新版本的特性,推荐使用 -
好的书写习惯
- 花括号:
- 每行长度不超过 80 个字符
- 缩进,额外的空行,分号
- 尽可能减少嵌套语句的出现
- 函数名能够精确表示函数的功能(动词)
- 一个函数只做一件事
- 令 函数 == 注释
- 函数位置在主程序下面,便于阅读
- 反面教材:忍者代码
变量⚓︎
let
关键字用于声明各种类型的变量- 推荐一行声明一个变量,方便阅读
- 同一个变量声明两次会报错
- 变量命名:类似 C 语言,但它还允许符号
$
,且$
和_
在命名时被视为字母(甚至可以用中文,但别这么做) (保留字列表) - 推荐的做法:使用易读的名称,避免使用缩写,名称能够准确描述变量功能,为不同的值使用不同的变量
,它与var
不要用let
和const
有以下区别:var
声明的变量没有块级作用域,即它们要么在当前函数可见,要么全局可见var
变量申明在函数开头就会被处理
const
关键字用于声明常量变量,这类变量无法被修改- 推荐的命名法:使用大写字母和下划线来命名,用于存储一些难以记住且确保不变的值
数据类型⚓︎
可以使用 typeof x
运算符返回变量 x
的类型。
-
number:整数、浮点数,以及一些「特殊数值
」 :Infinity
、-Infinity
、 NaN
(计算错误) ,安全范围为 \(-(2^{53} - 1) \sim (2^{53} - 1)\)- 如果数字特别长,可以用下划线
_
间隔,JS 会忽略数字之间的_
,这是一种语法糖 - 科学计数法
e
(同 C 语言) - 十六进制(前缀
0x
) 、八进制(前缀0o
) 、二进制(前缀0b
) - 使用
num.toString(base)
方法可以返回数字在 base 进制下的字符串表现形式(base
最大为 36) 。如果num
是数字字面量而非变量,需要用两个点(即..toString(base)
)或者用括号将数字包起来 - 关于舍入的数学函数
Math.ceil(num)
:向上取整Math.floor(num)
:向下取整Math.round(num)
:四舍五入Math.trunc(num)
:截断(小数部分)取整- 四舍五入至第 n 位:
Math.round(num * 1en) / 1en
,请将n
替换为数字Number(num.toFixed(n))
(.toFixed()
返回的是字符串)
- 同 C 语言,JS 也无法精确存储小数,使用
.toFixed()
方法对结果进行舍入可以缓解这种情况 isNaN(value)
函数:先将value
转化为数字型,然后判断它是否为NaN
,若是则返回true
,否则返回false
isFinite(value)
函数:先将value
转化为数字型,然后判断它是否为常规数字(不是NaN/Infinity/-Infinity
) ,若是则返回true
,否则返回false
(注意空字符串或仅包含空白字符的字符串转化为数字后都是 0)parseInt(str[, base])
函数:从字符串中提取(base
进制,可选参数)整数,直到无法提取为止,返回已提取的数字parseFloat(str)
函数:类似上面的函数,但返回的是浮点数- 其他内建函数(来自 Math 对象)
Math.random()
返回 \([0, 1)\) 之间的随机数Math.max(a, b, c...)
和Math.min(a, b, c...)
:分别返回参数中的最大值和最小值Math.pow(n, power)
:返回n^{power}
- 如果数字特别长,可以用下划线
-
bigint:任意长度的整数
-
string:可用单引号、双引号或反引号表示字符串
- 但是反引号的字符串允许将变量或表达式传进去(还需使用
${}
包装,类似 Python) ,还允许字符串跨行
- 转义字符(同 C 语言)
\uXXXX
使用 Unicode 编码的字符
.length
:字符串长度- 访问字符串指定字符
str[pos]
str.charAt(pos)
两者的区别是若没有找到字符,前者返回
undefined
,后者返回空字符串
- 遍历字符串的字符
for (let char of str)
(注意是of
) - 不可以直接修改字符串的内容
.toLowerCase()
和.toUpperCase()
方法分别使字符串的字母全变成小写字母和大写字母- 查找子字符串
str.indexOf(substr, pos)
:从给定位置pos
开始查找str
的子字符串substr
,若找到返回子字符串首次出现的位置,否则返回 -1str.lastIndexOf(substr, pos)
:类似上面的方法,但它是从字符串末尾开始逆向遍历str.includes(substr[, pos])
:也类似第一种方法,但pos
位置可选,且返回的是true
或false
str.startsWith(substr)
:检查字符串是否以substr
开头,若是则返回true
,否则返回false
str.endsWith(substr)
:检查字符串是否以substr
结尾,若是则返回true
,否则返回false
- 获取子字符串
str.slice(start[, end])
:返回str
从start
位置开始到end
位置为止(不包括end
)的子字符串,若只有一个参数则返回从start
开始剩余部分的子字符串。注意start
和end
可以是负数(-1
是最右边的字符索引,负数索引是从右往左数的,同 Python)str.substring(start[, end])
:与前一种方法的唯一区别在于不支持负参数(会被视为 0)str.substr(start[, length])
:与第一种方法的区别是第二个参数指定的是子字符串的长度而非结束位置(因此必须是非负值)
- 比较字符串
str.codePointAt(pos)
:返回字符串str
在pos
位置的字符的 ASCII 码String.fromCodePoint(code)
:返回 ASCII 码为code
的字符
- 但是反引号的字符串允许将变量或表达式传进去(还需使用
-
boolean:
true
、false
- null:未知值
- undefined:未定义的值(未被赋值 ...)
- symbol:对象的唯一标识符(见“对象 symbol 类型”一节)
- object
: (见“对象”一章)
类型转换⚓︎
String(value)
将value
转为字符串型- 数字型
- 自动转换:算术运算
- 显示转换:
Number(value)
,一些特殊规则:Number(undefined) -> NaN
Number(null) -> 0
Number(true) -> 1 Number(false) -> 0
- 字符串转数字会先去掉空白字符,若剩余部分为空返回 0,若无法转为数字返回
NaN
- 布尔型:
Boolean(value)
,除了0
、空字符串、null
、undefined
、NaN
返回false
,其余值都返回true
数组⚓︎
- 数组本质上是一种特殊的对象(特殊之处在于数组存储的是有序数据
) ,数组的元素可以是任意类型(对象、函数等均可) - 创建空数组(2 种方法)
- 初始化数组:
let fruits = ["Apple", "Banana", "Orange"];
- 访问并修改元素:
fruits[2] = 'Pear'
,注意无法使用负索引.at(i)
方法可以访问索引为i
的数组元素,i
可以是负数.pop()
:取出数组末端的元素.shift
:取出数组开头的元素
- 新增元素
- 若访问不存在的元素,则会往数组添加元素,比如
fruits[3] = 'Lemon'
.push(elem)
:在数组末尾添加元素elem
.unshift(elem)
:在数组开头添加元素elem
注:
pop()
和push()
的速度快于unshift()
和shift()
(字面意思,后两种方法还多了一步 O(n) 的移位操作)
- 若访问不存在的元素,则会往数组添加元素,比如
.length
:数组元素个数- 我们可以手动修改
.length
的值:增加不会发生变化,减小则会截断数组(不可逆) - 清空数组:
arr.length = 0;
- 我们可以手动修改
- 引用复制:
let arr = fruits;
- 数组的错误使用方法
- 添加非数字的属性,比如
arr.test = 5;
- 制造空洞,比如添加
arr[0]
后就添加arr[1000]
,中间没有元素 - 倒序填充数组
- 添加非数字的属性,比如
- 遍历数组:使用
for...of
循环(不要用)for...in
循环 - 多维数组
String(arr)
:数组 -> 字符串,该字符串是一个用逗号间隔的元素列表- 不要使用
==
比较数组 -
.splice()
方法:- 语法:
arr.splice(start[, deleteCount, elem1, ..., elemN])
,其中只有start
是必写参数,它可以是负数(使用负索引) - 功能:从索引
start
开始修改arr
,删除deleteCount
个元素后,在当前位置插入新的元素elem1, ..., elemN
,最后返回修改后的数组 - 实际运用:
- 语法:
-
arr.slice([start], [end])
方法:获取start
到end
(不包括end
)之间所有的数组元素,返回由这些元素构成的数组。两个参数可以是负数,且都是可选参数(参数表为空则复制整个数组) -
arr.concat(arg1, arg2...)
方法:返回由原数组与传入参数(可以是任何值(对象也行)和数组(只会复制数组的元素,不复制完整实体) )拼接而成的新数组如果对象有
Symbol.isConcatSpreadable
属性,将对象视作数组拼接 -
.forEach()
方法:为数组的每个元素都运行一个函数- 语法(其中三个参数分别为数组元素、索引、数组
) :
- 括号里面的函数既可以是内建函数、箭头函数等
- 变体:
.map()
方法,语法类似,但注意函数返回值是新的值而不是当前元素,且调用该方法返回的是数组元素经过函数修改后的新数组 arr.indexOf(item, from)
:从索引from
开始搜索item
,若找到则返回索引,否则返回 -1arr.lastIndexOf(item, from)
:从索引from
开始逆向(从右往左)搜索item
,若找到则返回索引,否则返回 -1arr.includes(item, from)
:类似上一种方法,但返回的是true
或false
.find()
方法:用于对象数组,寻找具有特定条件的对象- 语法同
.forEach()
方法 - 但是函数体的内容有讲究:如果找到符合条件的对象则返回
item
并停止迭代;如果遍历完后都找不到则返回undefined
- 变体:
.findeIndex()
返回的是索引.findLastIndex()
逆向搜索,返回索引.filter
返回的是所有匹配条件的元素构成的数组,而不是第一个符合条件的元素
.sort([cmp_func])
方法:对数组进行排序(注意这个操作修改的是原数组的内容,而非数组的副本)- 如果括号内没有参数,则会将所有元素视为字符串,按照字符串比较规则升序排序
- 括号内可以指定比较函数,用来确定排序规则,比如:
function compareNumeric(a, b) { if (a > b) return 1; if (a == b) return 0; if (a < b) return -1; } // 调用,现在该方法是按照数字大小排序的了 arr.sort(compareNumeric);
事实上,比较函数返回正值表示“大于”,返回负值表示“小于”,所以有以下的精简版:
- 语法(其中三个参数分别为数组元素、索引、数组
-
arr.reverse()
:逆转数组的顺序 str.split(delim[, length])
:将字符串str
按照分隔字符delim
划分元素并形成一个数组- 第二个参数
length
控制划分元素的个数,很少用 - 如果
delim = ''
,该方法将每个字符作为单独的数组元素
- 第二个参数
arr.join(glue)
:将数组元素连接成一个字符串,元素之间用glue
分隔-
.reduce()
方法:让每个数组元素运行指定函数,返回运行函数后的结果- 语法:
let value = arr.reduce(function(accumulator, item, index, array) { // ... }, [initial]); // accumulator:累加器,保存上一次调用函数的结果 // item:数组元素 // index:索引 // arr:数组 // initial:accumulator 的初始值 // 函数返回值为最后一次调用函数后 accumulator 的值
- 应用:
-
Array.isArray()
方法:判断变量是否为数组,返回布尔值 - 除了
.sort()
方法外,上述大多数调用函数的方法都有第二个参数thisArg
。当第一个参数func
函数出现this
时,如果没有第二个参数指定this
对应的对象就会报错,所以需要thisArg
显式指出对象
映射⚓︎
映射是一个带键的数据项的集合,类似对象,但它允许任何类型(包括对象,甚至 )的键。NaN
相关方法和属性:
new Map()
:创建映射map.set(key, value)
:为指定键设置值-
map.get(key)
:获取特定键的值(若映射内不存在key
,返回undefined
)注:我们一般用
set
和get
访问映射内的元素。虽然可以用方括号表示法来访问,但这不是正确的使用方法,不推荐这样用。 -
map.has(key)
:若key
存在返回true
,否则返回false
map.delete(key)
:删除指定键的值map.clear()
:清空映射map.size
:返回映射内的元素个数
例子
-
上述的方法可以链式调用,如下所示:
-
映射的迭代(遍历顺序与插入时顺序相同
) :map.keys()
:遍历并返回一个包含所有键的可迭代对象map.values()
:遍历并返回一个包含所有值的可迭代对象map.entries()
:遍历并返回一个包含所有键值对([key, value]
)的可迭代对象(for...of map
默认使用这个)map.forEach(value, key, map) => {...}
:为每个键值对调用一个函数,与数组的类似
例子
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// 遍历所有的键(vegetables)
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}
// 遍历所有的值(amounts)
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// 遍历所有的实体 [key, value]
for (let entry of recipeMap) { // 与 recipeMap.entries() 相同
alert(entry); // cucumber,500 (and so on)
}
- 对象 -> 映射:
let map = new Map(Object.entries(obj))
- 映射 -> 对象:
let obj = Object.fromEntries(map);
集合⚓︎
集合是一个值的集合,每个值仅出现一次。
相关方法和属性:
new Set(iterable)
:创建集合,将可迭代对象(通常是数组)的值复制到集合内-
set.add(value)
:添加一个值,返回修改后的集合注:重复添加同一个值不会使集合有任何变化
-
set.delete(value)
:删除值,如果删除存在的值则返回true
,否则返回false
set.has(value)
:若value
在集合内,返回true
,否则返回false
set.clear()
:清空集合set.size
:返回元素个数
例子
let set = new Set();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
// visits,一些访客来访好几次
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
// set 只保留不重复的值
alert( set.size ); // 3
for (let user of set) {
alert(user.name); // John(然后 Pete 和 Mary)
}
集合的迭代:与映射使用相同的方法,但有以下不同点:
set.keys()
等同于set.values()
(集合里没有键)- 出现
key
的地方可以替换为value
(所以会出现两次value
的情况,这是为了兼容映射)
运算符⚓︎
与 C 语言相同的部分就不再赘述,这里着重介绍 JS 特有的部分。
- 求幂
**
(同 Python) +
可拼接字符串,而且如果+
两边有一边是字符串,那么另一边会被转化为字符串然后被拼接起来(比如'1' + 2 + 2 == '122'
) 。而其他运算符都是将两边的变量转为数字型+
作为一元运算符,用于非数字变量前的作用与Number()
一致,将其转为数字型(比如+"2" + +"3" == 5
)- 链式赋值:多个变量用赋值号
=
连接,赋值顺序从右向左(同 Python) - 也有自增(
++
)/ 自减(--
)运算符 - 位运算符多了一个无符号右移(
>>>
) - 运算符优先级表
- 值的比较
- 注意字符串比较规则(同 C 语言)
- 不同类型的比较:先全部转为数字型再比较大小
- 比较运算符几乎同 C 语言,但它多了个严格相等运算符
===
,它就不会先将两侧的值转化为数字,而是直接比较(符合我们的直觉) 。同样也多了对应的严格不相等!==
Object.is(val1, val2)
类似===
,但有以下区别:Object.is(NaN, NaN)
的值为true
,NaN === NaN
返回false
Object.is(0, -0)
的值为false
,0 === -0
返回true
条件分支⚓︎
if
、else
、else if
- 条件(三目)运算符
?
- 逻辑运算符:
||
、&&
、!
- 注意“短路求值”的概念
- 空值合并运算符
??
- 用法:
a ?? b
,当a
不是null
或undefined
时返回a
,否则返回b
- 可以连续使用多个
??
从一系列的值中返回第一个非null
或undefined
的值 - 不可以与
&&
和||
一起用,除非用括号隔开
- 用法:
switch
语法类似 C 语言,但要注意以下几点:case value:
中的value
可以为任意表达式- 采用的是严格相等的比较方式
循环⚓︎
for
while
do...while
-
break
、continue
- 它们不能与条件运算符一起使用
- 它们还有一种与 C 语言
goto
类似的用法
交互⚓︎
alert(msg)
函数:运行该函数后会显示一个名为模态窗 (modal window) 的东西,内容为 msg(字符串) ,直到点击“确定”按钮后用户才能与页面交互prompt(title, [default])
:显示内容为title
的模态框,还有一个输入框和确认 / 取消按钮。它会将我们输入的内容作为函数返回值,如果点击取消或直接退出(Esc
) ,返回值为null
- 第二个参数
default
可选,作为默认的输入内容出现在输入框上,推荐使用
- 第二个参数
confirm(question)
:显示内容为question
的模态框,带确认 / 取消按钮,点击确认返回true
,点击取消或直接退出返回false
函数⚓︎
基础部分⚓︎
- 函数声明:
- 函数表达式:在任何表达式的中间声明函数,这时函数可以省略函数名(匿名函数,我们无法直接访问这类函数)
函数声明 vs 函数表达式
一些区别:
- 函数表达式是在代码执行到达时被创建,在这之后我们才可以调用它;而函数声明可以在它声明之前就可以被调用,只要调用语句与函数声明位于同一代码块内
- 然而,如果调用语句在函数声明所处的代码块外,那么就会报错。解决方法是在代码块外声明变量,在代码块内对该变量使用函数表达式,这样在代码块外通过调用这个变量(实际上成为函数了)来调用函数
- 局部变量、外部变量(全局变量尽量少用)
- 形参 (parameter):函数声明中括号内列出的变量,实参 (argument):函数调用时传给函数的值,可以为形参设置默认值(可以是复杂的表达式,甚至是函数)
- 如果某个参数没有传入值,该参数为
undefined
-
return
返回值return;
和不写return
一样,最终返回的是undefined
- 不要在
return
和返回值之间添加新行,因为在同一行上,若return
后面没有内容,JS 自动会加上分号
-
函数是一个值
- 可以将一个函数赋给变量,那么该变量就成为与该函数一样的函数(比如
let func = sayHi
后, func() == sayHi()
) alert(func)
的结果是显示func()
函数的源码
- 可以将一个函数赋给变量,那么该变量就成为与该函数一样的函数(比如
- 回调函数:简单理解为作为参数值的函数
例子
下面代码中,showOk()
和 showCancel()
都是回调函数:
function ask(question, yes, no) {
if (confirm(question)) yes();
else no;
}
function showOk() {
alert("You agreed.");
}
function showCancel() {
alert("You canceled the execution.");
}
ask("Do you agree?", showOk, showCancel);
我们可以使用匿名函数简写上面的回调函数:
-
箭头函数
-
格式:
let func = (arg1, arg2, ..., argN) => expression
,等价于下面的函数表达式: -
如果只有一个参数,可以省略圆括号
- 如果没有参数,必须留下空括号
- 多行的箭头函数:使用花括号,最后要用
return
语句返回值,比如:
this
,所以若访问this
,函数会尝试从外部获取对象 - 不能将箭头函数作为构造函数,即不能使用new
操作符 - 箭头函数没有arguments
变量 -
变量作用域、闭包⚓︎
- 代码块:在代码块
{...}
内(包括if
、for
、while
等)声明的变量只有在该代码块内可见 - 嵌套函数:一个函数创建于另一个函数内
- 函数还可以返回一个嵌套函数
-
词法环境:由环境记录(environment record)(一个存储所有局部变量作为其属性的对象)和对外部词法环境的引用构成
- 全局词法环境:没有外部引用的词法环境
例子
这个例子展示了代码执行过程中全局词法环境的变化:
- 函数声明:虽然函数和变量一样也是一个值,但不同于变量的是只要有函数声明,函数就已经创建完毕,不像变量要到执行到声明语句后才会被创建。下面这张图体现了函数和变量的这点区别:
- 多个词法环境:当函数开始被调用时,JS 会自动创建一个新的词法环境,用于存储该函数的局部变量和参数。此时程序分为内部词法环境和外部词法环境
- 程序访问变量的顺序:内部词法环境 -> 外部词法环境 -> ...(更外部的词法环境)-> 全局词法环境
例子
该例子展示了程序如何找到变量对应的值
- 所有函数都有名为
[[Environment]]
的隐藏属性,该属性保存了对创建该函数的词法环境的引用
-
闭包 (closure):一个函数可以记住其外部变量并且可以访问这些变量。JS 中所有函数都是闭包的,它们自动通过隐藏的
[[Environment]]
属性记住创建它们的位置,所以它们都可以访问外部变量。
setTimeout 和 setInterval⚓︎
setTimeout()
函数:延迟执行指定函数- 语法:
setTImeout(func|code[, delay, arg1, arg2,...])
func
:想要执行的函数或代码块delay
:执行前的延迟时间(单位:ms)arg1, arg2...
:func
的参数列表
- 返回值是一个定时器标识符 (timer identifier),可以用它来取消该函数的执行——执行
clearTimeout(timerId)
即可(timerId
为该函数的返回值) - 零延时调用
setTimeout(func, 0)
或setTimeout(func)
,使该函数在当前脚本执行完时调用(事实上至少还有 4ms 延迟)
- 语法:
setInterval()
函数:间隔指定时间,周期性执行指定函数- 语法同上面的函数
- 同样也可以阻止该函数的调用——执行
clearInterval(timerId)
可以使用嵌套的 setTImeout
。这种嵌套方法相比 setInterval()
,在两次执行之间的延时的设置上更加灵活精确。
setInterval(func, delay);
// 等价于
setTimeout(function run() {
// func 函数体
setTimeout(run, delay);
}, delay);
下面两张图分别展示它们在时间间隔上的微妙差异(上图为 setInterval()
,下图为嵌套的 setTimeout()
,且 delay = 100
关于垃圾回收
- 当一个函数传入
setInterval()
或setTimeout()
时,JS 会为该函数创建一个内部引用,这样该函数就无法被垃圾收集器回收,直到clearInterval()
或clearTimeout()
被调用为止 - 如果该函数还引用了外部变量(形成一个闭包
) ,那么只要该函数存在,外部变量就一直存在,可能比该函数占用更多的内存
因此建议在程序最后使用 clearInterval()
或 clearTimeout()
错误处理⚓︎
-
基本语法:
try...catch
try { // 先执行这里的代码 } catch (err) { // 若没有错误,则忽略这里的语句 // 若出现错误,则跳过 try 剩余语句,执行 catch 块内的雨具 // err 表示一个 error 对象 }
catch
只能捕捉运行时出现的错误 (runtime errors)(或者叫做异常 (exceptions)) ,如果有语法错误,程序将无法执行- 如果
try...catch
内执行setTimeOut
之类的函数,异常不会被捕获到。如果需要捕获异常,需要将try...catch
作为setTimeOut
的函数参数 catch
部分的(err)
可忽略(如果不用err
的话)
-
Error 对象
-
相关属性有:
name
:error 名称,比如ReferenceError
message
:error 详细描述stack
:当前的调用栈- ...
例子
try { lalala; // error, variable is not defined! } catch (err) { alert(err.name); // ReferenceError alert(err.message); // lalala is not defined alert(err.stack); // ReferenceError: lalala is not defined at (...call stack) // 也可以将一个 error 作为整体显示出来 // error 信息被转换为像 "name: message" 这样的字符串 alert(err); // ReferenceError: lalala is not defined }
-
创建错误对象
- 其中
Error
是标准 error 构造函数的一种,同时作为error.name
,还包括SyntaxError
、ReferenceError
、TypeError
等等 message
作为error.message
使用
- 其中
-
-
主动抛出错误:
throw <error object>;
,执行这条语句后就进入catch
块内 - 再次抛出 (Rethrowing):
catch
应该只处理它知道的 error,并抛出所有其他的 error
例子
-
try...catch...finally
- 如果
try
语块内存在return
语句,那么后面的finally
从句还是会执行的 - 可以省略
catch
,使用try...finally
:当try
语块内发生异常时就直接执行finally
中的代码
例子
let num = +prompt("输入一个正整数?", 35) let diff, result; function fib(n) { if (n < 0 || Math.trunc(n) != n) { throw new Error("不能是负数,并且必须是整数。"); } return n <= 1 ? n : fib(n - 1) + fib(n - 2); } let start = Date.now(); try { result = fib(num); } catch (err) { result = 0; } finally { diff = Date.now() - start; } alert(result || "出现了 error"); alert( `执行花费了 ${diff}ms` );
- 如果
对象⚓︎
对象用于存储键值对或更复杂的实体。它是一个由花括号包起来的属性列表,一个属性就是一个键值对(key:value
symbol
值(后面会讲到
-
创建空对象:
-
创建带键值对的对象:
我们可以进行以下操作:
- 访问属性值:
user.name
、user.age
- 加入新的属性:
user.isAdmin = true;
- 移除属性:
delete user.age;
- 访问属性值:
-
方括号访问法(
obj["property"]
) vs.
访问法(obj.property
) :-
属性名可以由多个单词构成,但需要用引号括起来,比如
"likes birds": true;
。如果要访问这样的多词属性,需要用方括号替代.
,比如user["likes birds"]
。 -
方括号访问元素比用
.
访问元素更加灵活,因为只要方括号内的表达式的值为键名(字符串) ,那么就可以成功访问该属性,而.
访问法就不能这样,比如:
let key = prompt("What do you want to know about the user?", "name"); alert( user[key] ); // output the inputted name alert( user.key ); // undefined
- 在对象字面量内使用方括号,这样的属性称为计算属性。看下面的例子,
[fruit]
属性名(键)应该从fruit
变量获取。方括号内还可以是一个复杂的表达式
let fruit = prompt("Which fruit to buy?", "apple"); let bag = { [fruit]: 5, // 属性名是从 fruit 变量中得到的 }; alert( bag.apple ); // 5 如果 fruit="apple"
- 总结:
[]
虽然强大但很麻烦,能够用.
访问法就用它,实在不行用[]
-
-
属性名的命名更加自由:只要不是保留字的字符都会被接受(
甚至可以是一串数字) ,JS 会自动将名称转化为字符串 - 如果属性名和作为它对应值的变量的名称相同,则可以进行属性值简写,比如一个键值对
name: name
等价于name
in
操作符:判断属性是否存在,语法:"key" in object
,其中key
为属性名,object
为被检测的对象for
循环:for (key in object) { ... }
,这样会遍历对象所有的键- 属性的顺序问题:如果属性名是一个整数(只有数字字符
) ,JS 会自动根据整数值进行升序排序,否则就按照创建时的顺序排序(对对象进行遍历的时候就会发现这个顺序)
对象引用和复制⚓︎
对象引用:实际上,一个赋值了对象的变量并不是一个对象,它仅保存了该对象在内存中的地址,也就是该对象的引用。因此对于下列语句:
admin
和 user
是同一个对象的引用。如果用 admin
修改这个对象的属性,那么用 user
也可以访问该对象修改后的属性。
Object.assign
- 语法为
Object.assign(dest, [src1, src2, src3...])
,其中dest
是目标对象,后面的可选参数为源对象,该方法会将所有源对象的属性拷贝到目标对象内,最后返回dest
- 如果
dest
存在与源对象名称相同的属性,则该属性会被源对象的属性覆盖 - 使用该方法实现浅拷贝:
let clone = Object.assign({}, obj)
要实现深拷贝,需要用到
.cloneDeep(obj)
函数
方法⚓︎
方法:作为对象属性的函数
let obj = {
// ...
};
/* 方法声明 */
// 法1:函数表达式
obj.method = function() {
// ...
}
// 法2:函数声明 + 变量赋值(略)
// 法3:
obj = {
method: function() {
// ...
}
};
// 法4:
obj = {
method() {
// ...
}
};
/* 方法调用 */
obj.method()
/* 复制函数 */
function func() {
// ...
}
obj.method = func;
this
关键字:使对象方法能够访问对象中存储的信息,通过 this.property
可以访问对象的属性。
- JS 的
this
可用于任何函数中(即使不是对象的方法) ,JS 会根据代码上下文来计算this
的值,也就是说this
的值是在程序运行时获得的 - 如果
this
没有表示任何对象的话,它的值为undefined
(严格模式下) - 箭头函数没有自己的
this
,所以若在箭头函数内使用this
,this
的值取决于外部正常的函数
垃圾回收⚓︎
JS 主要的内存管理概念是「可达性
- 根 (roots):
- 当前执行函数的变量和参数
- 嵌套调用链上的其他函数及其变量和参数
- 全局变量
- ...
- 来自对根的引用或引用链
如果值不具备可达性,那么 JS 的垃圾回收器就会自动删除这些值
一个复杂的例子
我们构建了一组相互关联的对象:
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
});
此时的内存结构图:
若我们移除两个引用:
现在没有对 John
的引用了,所以它变得不可达,那么 JS 就会自动将它从内存中删掉,实现垃圾回收。
构造函数⚓︎
若要创建多个类似的对象,较好的方法是使用构造函数来做,它能够实现可重用的对象创建代码,做到“一劳永逸”,无需一遍遍地使用字面量创建。这类函数与常规函数的区别有:
- 函数名以大写字母开头(约定)
- 只能由操作符
new
来执行
// 构造函数声明
function User(name) {
// this = {};
this.name = name;
this.isAdmin = false;
this.sayHi = function() {
alert("My name is: " + this.name);
};
// return this;
}
// 调用构造函数
let user = new User("Jack");
// 可以像这样创建多个类似的对象
作为构造函数的函数无需 return
语句,因为默认有 return this;
。但我们也可以手动加上 return
语句,JS 处理规则如下:
- 如果返回的是一个对象,则返回该对象而不是
this
- 如果返回其他类型的值,则忽略该
return
语句,返回this
特殊用法:new function() {...}
在调用构造函数的位置创建函数,这样的函数是匿名函数,因此无法在后面被调用,这样的作用是处理构造单个对象时的复杂操作(而且后面也用不到了
new.target
可以检查该函数是否用 new
来调用过(也就是有没有成为构造函数undefined
,如果是 new
调用则返回函数源码(从 function
到 }
的字符串)
可选链 ?.⚓︎
对象属性的 .
访问法有一个问题:如果 .
两边的值有一个不存在(null
或 undefined
?.
来替代 .
(value?.prop
- 只要
?.
前面的值为null
或undefined
,它就会停止运算并返回undefined
(这里就出现了「短路效应」 ,类似逻辑运算符) - 否则就会返回
value.prop
,即使不存在prop
属性也不会报错,而是返回undefined
在使用 ?.
要注意以下几点:
?.
可以安全地读取和删除对象的数据,但不能写入(否则会报错) :
- 不要过度使用可选链,只有当某个属性是可选的(即它可能在对象内,也可以不在对象内)时候,使用
?.
是合理的。能不用?.
尽量不用,还是用.
?.
前的变量必须已声明,否则会报错
可选链的其他变体:
?.()
:调用可能不存在的函数?.[]
:类似?.
,读取可能不存在的属性(成功读取等价于[]
访问法)
Symbol 类型⚓︎
symbol
值表示对象唯一的标识符(symbol 名相同的两个 symbol
值是不相等)
-
创建
let id = Symbol("name")
- 在对象字面量内创建,需要用方括号将 symbol 名包起来:
-
symbol
值无法被自动转化为字符串.toString()
方法可以将symbol
转为字符串显示- 访问
.description
属性可以显示 symbol 名的字符串
symbol
值可以作为对象的“隐藏”属性,这种属性无法被意外访问或重写symbol
属性不参与for...in
循环Object.key(obj)
会忽略symbol
属性,而Object.assign()
会同时复制字符串和symbol
属性- 全局 symbol 注册表:每次访问该表内相同名字的 symbol 时都会返回相同的 symbol,做到同名的 symbol 是相同的 symbol
Symbol.for("name")
:访问全局注册表内名为name
的symbol
值,如果存在则读取出来,否则在注册表内创建该symbol
Symbol.keyfor(sym)
:在全局注册表中查找symbol
变量的 symbold 名(键) ,对于非全局 symbool 则返回undefined
// Symbol.for() 用法
let id = Symbol.for("id");
let idAgain = Symbol.for("id");
alert (id === idAgain) // ture
// Symbol.keyfor() 用法
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
alert(Symbol.keyFor(globalSymbol)); // name
alert(Symbol.keyFor(localSymbol)); // undefined
- 系统 symbol,例如
Symbol.hasInstance
、Symbol.toPrimitive
等等,具体见这个规范,后面的章节也会介绍一些常用的系统 symbol
对象 -> 原始值转换⚓︎
对象 -> 原始值转换的几种情况(后面三种转换被称为 hint
- 所有对象在布尔表达式中均被视为
true
- 数字转换
- 显示转换:
Number(obj)
- 数学运算:比如
Date
对象之间做减法,比较运算等 - 使用内建的数学函数
- 显示转换:
- 字符串转换
- 使用
alert(obj)
输出对象内容 - 将对象作为属性键:
anotherObj[obj] = 123
- 使用
default
转换,当 JS 无法确定进行哪一类转换时采用这种转换,往往是运算符不适合用于对象的情况(比如两个对象相加,或者将对象与其他类型做==
比较等)
JS 的转换流程:
-
如果
obj[Symbol.toPrimitive](hint)
方法存在的话(需要我们自己声明)则调用它Symbol.toPrimitive
是一种内建 symbol,它的作用是给转换方法命名,格式为:
obj[Symbol.toPrimitive] = function(hint) { // hint = "stirng" | "number" | "default" 三者之一 // 这里是将对象转换为原始值的代码 // 必须返回一个原始值 }
例子
let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // 转换演示: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500
-
否则若 hint 是
string
,则先尝试调用obj.toString()
,后尝试调用obj.valueOf()
-
否则若 hint 是
number
或default
,则先尝试调用obj.valueOf()
,后尝试调用obj.toString()
.toString()
方法返回一个字符串"[object Object]"
.valueOf()
方法返回对象自身- 这两个方法可以进行自定义
JSON⚓︎
JSON(JS 对象表示法)是基于 JS 对象语法的数据格式,但它独立于 JS 存在(因此在很多别的语言也可以读取和生成 JSON
{
"squadName": "Super hero squad",
"homeTown": "Metro City",
"formed": 2016,
"secretBase": "Super tower",
"active": true,
"members": [
{
"name": "Molecule Man",
"age": 29,
"secretIdentity": "Dan Jukes",
"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
},
{
"name": "Madame Uppercut",
"age": 39,
"secretIdentity": "Jane Wilson",
"powers": [
"Million tonne punch",
"Damage resistance",
"Superhuman reflexes"
]
},
]
}
- 访问 JSON 对象和访问一般对象的方法一致(点表示法 / 方括号表示法)
- JSON 字符串外部还可以套个方括号,形成 JSON 数组,用数组索引访问数组元素
[
{
"name": "Molecule Man",
"age": 29,
"secretIdentity": "Dan Jukes",
"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
},
{
"name": "Madame Uppercut",
"age": 39,
"secretIdentity": "Jane Wilson",
"powers": [
"Million tonne punch",
"Damage resistance",
"Superhuman reflexes"
]
}
]
- JSON 是一种纯数据格式,只包含属性,没有方法
- JSON 要求在字符串和属性名周围使用双引号,不得用单引号
- 只有带双引号的字符串才能作为 JSON 属性(所以转为 JSON 的属性自动添加双引号)
- JSON 的值可以是数字、带双引号的字符串、布尔值、数组等,不能是函数
- JSON 不支持注释
-
关于 JSON 的函数
-
JSON.stringify(value[, replacer, space])
:将对象转换为 JSON 字符串,这个过程被称为序列化 (serialization)value
包括:对象,数组,甚至基本类型(字符串、数字、布尔值、 null
)-
replacer
: (可选)表示要被转换的属性数组或映射函数function(key, value)
,只有这些指定的属性才能被转换- 若属性数组过长,可用自定义的映射函数制定规则,排除不要的属性
例子
let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // meetup 引用了 room }; room.occupiedBy = meetup; // room 引用了 meetup alert( JSON.stringify(meetup, function replacer(key, value) { alert(`${key}: ${value}`); return (key == 'occupiedBy') ? undefined : value; })); /* key:value pairs that come to replacer: : [object Object] title: Conference participants: [object Object],[object Object] 0: [object Object] name: John 1: [object Object] name: Alice place: [object Object] number: 23 occupiedBy: [object Object] */
-
space
: (可选)优化格式化的空格数量,是一个整数值(如果值为 2,嵌套对象内部缩进 2 个空格) - 一些特定的 JS 属性会被
JSON.stringify
跳过,包括:函数(方法)属性、Symbol
类型的键和值、存储undefined
的属性 - 支持嵌套对象转换
- 不能转换有循环引用(两个对象互有一个来自对方的属性)的对象
- 对象内部提供
toJSON
进行 JSON 转换,JSON.stringify
会自动调用它。我们也可以自定义toJSON
-
JSON.parse(str[, reviver])
:将 JSON 字符串转换为对象str
:JSON 字符串reviver
: (可选)函数function(key, value)
,为每个 JSON 键值对调用,并可以对值进行转换
例子
let schedule = `{ "meetups": [ {"title":"Conference","date":"2017-11-30T12:00:00.000Z"}, {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"} ] }`; schedule = JSON.parse(schedule, function(key, value) { if (key == 'date') return new Date(value); return value; }); alert( schedule.meetups[1].date.getDate() );
-
类⚓︎
类的构成:
- 构造函数
- 实例方法和实例字段
- 静态方法和静态字段
class MyClass {
// 构造函数
constructor() {
// ...
}
// 实例字段
myField = ...;
// 实例方法
myMethod() {
// ...
}
// 静态字段
static myStaticField = ...;
// 静态方法
static myStaticMethod() {
// ...
}
// 静态块
static {
// ...
}
// 私有形式
#myPrivateField = ...;
}
注
下面的代码等价于上面的定义:
function MyClass() {
this.myField = ...;
}
MyClass.myStaticField = ...;
MyClass.myStaticMethod = function () {
// ...
};
MyClass.prototype.method = function () {
// ...
}
- 构造函数:当使用
new
运算符创建类的实例时,就会自动调用构造函数- 构造函数里的
this
指向新创建的实例,并作为调用new
函数的结果返回 - 不建议用
return
任何值,它会作为new
函数的结果,覆盖掉this
的值
- 构造函数里的
- 实例方法:可用于读取、写入实例的某些值
创建类的实例(对象
类表达式:
// 无名类
const MyClass = class {
// ...
};
// 也可以带名字,但只在类里面可见
const MyClass1 = class MyClassFirst {
// ...
};
// new MyClassFirst(); // Error
私有与公共字段⚓︎
-
私有字段:以
#
标识,无法在类声明的外部被直接访问(会被认为是语法错误)- 声明私有字段的好处是使类得到较好的封装,可以防止用户直接访问类的底层数据,避免意外发生,从而提升代码的鲁棒性
- 只要属于同一个类,实例 A 的方法还是可以读取其他实例的私有字段
例子
class Color { #values; constructor(r, g, b) { this.#values = [r, g, b]; } redDifference(anotherColor) { // #values 不一定要从 this 访问: // 你也可以访问属于同一个类的其他实例的私有字段。 return this.#values[0] - anotherColor.#values[0]; } } const red = new Color(255, 0, 0); const crimson = new Color(220, 20, 60); red.redDifference(crimson); // 35
- 如果某个类实例访问另一个类实例(不同的类)中不存在的私有字段,JS 会抛出异常,可以使用
in
运算符判断某个实例是否存在某个私有字段
例子
- 私有字段不得重复声明,也无法被
delete
删除,这些均会导致语法错误 - getter 和 setter 函数(接下来会讲到)也可以是私有的(函数名前加个
#
)
-
公共字段:实例可以直接获取公共字段的值
- 在类里面,公共字段的声明就像一般的变量,独立于构造函数之外,但它的值也自动赋值给
this
了
- 在类里面,公共字段的声明就像一般的变量,独立于构造函数之外,但它的值也自动赋值给
getter 和 setter⚓︎
例子
可以看到,如果在类中有一个匹配的 getter 和 setter 字段,那么就好像为这个类创建一个新的「属性
如果只设了 getter 而没有设置 setter,那么这个「属性」是只读的,不能修改的(若修改则会抛出类型错误
静态属性⚓︎
静态属性包括静态方法、静态字段和静态的 getter 和 setter 函数。它们不属于任何一个类的实例,仅在类定义里面使用,无法直接被实例访问。
例子
静态初始化块:在类的第一次加载时运行的代码块,此时可以访问静态的私有属性
例子
扩展与继承⚓︎
派生类 (derived class):由父类继承而来的类,可以访问父类的所有公共属性(包括静态属性extends
子句声明。
- 在构造函数内,如果要访问
this
,在此之前一定要先调用super()
函数:调用父类的构造函数来初始化this
例子
- 派生类可以覆盖父类的方法
- 使用
super.method()
来访问父类的方法method()
,可以在修改方法时减少代码量
例子
- 派生类无法访问父类的私有字段
- 一个类至多只能扩展自一个父类
- 派生类的实例同时也是父类的实例(可用
instanceof
运算符验证)
const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color instanceof Color); // true
console.log(color instanceof ColorWithAlpha); // true
JS 抽象行为大赏
typeof null
的返回值为object
0 == "0" -> true
(因为"0"
被转为数字0
了)0 == '' -> true
(原因同上)null == undefined -> true
(原因同上)null >= 0 -> true null == 0 -> false
(前者原因同上,后者因为在相等比较中null
不会转化为数字)0.1 + 0.2 == 0.3 -> false
(JS 无法精确存储小数,这是很多语言的通病)NaN === NaN -> false
(每个NaN
值都是独一无二的)[] == [] -> false
(JS 认为这两个数组是不同的对象,只有对同一个数组的引用才是相等的)
评论区