JS 基础语法
本文参考内容 https://www.bilibili.com/video/BV1UR4y1m7SB/
行末分号
以下字符开头的代码,前一行行末不能省略分号,可以在行首加上
[(方括号)((圆括号)`(反引号)/(斜杠)+,-等一元或二元运算符
以下是部分常见的行首增加分号的情况
- 使用 ES6 解构对变量进行交换
// 数组乱序 const arr = [1, 2, 3, 4, 5, 6, 7] for(let i = arr.length - 1; i > 0 ; i--) { const j = Math.floor(Math.random() * (i+1)) // 0-i 的一个索引值,实现可能交换或不交换 ;[arr[j], arr[i]] = [arr[i], arr[j]] // 数组解包,类似 py - 此处由于开头满足条件,在行首使用分号 } - 自执行函数
;(function(){ console.log(1) })() ;(function(){ console.log(1) }()) - 非数组对象调用数组方法
let res = '' const str = 'abc' ;[].forEach.call( // `[` 开头,需要分号 str, c => {res += "\\x" + c.charCodeAt(0).toString(16)} ) console.log(res)
基本类型
部分基本类型相关用法介绍
- bigInt
let a = 99999999999999999999999n // 防止整数过大时精度丢失 let b = 99999999999999999999999 console.log(a,b) - 长字符串换行
console.log('aaaaaaaa\ bbbbbbbbaaaaaaaa\ ccccccccbbbbbbbbaaaaaaaa') // 与python的三引号多行字符串类似,前方不能有空格,否则字符串中也会包含空格
逻辑符
使用 || && 控制代码执行
func1() || func2(): 当第一个函数返回真时,第二个函数不执行func1() && func2(): 当第一个函数返回假时,第二个函数不执行
在此基础上,无括号优先级的连续逻辑符,则按从左至右的顺序,依次判断执行到哪个函数停止
E.g. func1() || func2() && func3() || func4()
- 根据
func1返回是否为真判断后续函数是否执行 - 再根据
func2返回为假判断后续函数是否执行 - ...
每个函数的结果只影响身后的逻辑符
数组
- 声明
- 隐式创建 / 字面量声明
var arr = [] - 构造函数声明,可传入数字表明声明数组的长度
var arr = new Array() // 若使用 new Array(5), 此时是默认填充 5个 undefined 的数组 - 省略
new的构造函数声明var arr = Array()
- 隐式创建 / 字面量声明
- 下标
arr.length = 99 // 修改数组长度,此时会填充 undefined console.log(arr[0]) console.log(arr.at(-1)) // at 与方括号类似,但多了负索引的支持 // 越界,不会报错,返回 undefined console.log(arr[101]) - 数组内容: 元素内容可执行时,读取时会直接返回执行结果
var arr = [1+2, function(){return 1}()] console.log(arr[0], arr[1]) // 3 1
常用函数
- 元素增删
push/pop组尾操作 推入/弹出shift/unshift组首操作 拿出/放入
- 拼接
var res = arr.concat([1,2,3]) // arr 不会被修改,需要返回值接收 - 搜索. 参数1 目标值, 参数2 从哪个索引开始找
arr.indexOf(1) // 不存在返回 -1 arr.lastIndexOf(1) // 从右往左 - 是否包含
arr.includes(1) - 拼接成字符串
arr.join()无参数默认是逗号 - 截取
// 参数 [index1, index2] [) 左闭右开 , 不会修改 arr,返回值需要接收 var res = arr.slice(1, 3) var res = arr.slice() // 都不传默认是浅copy原数组 - 修改
splice参数1 索引 参数2 删除个数 参数345... 添加元素内容// 从索引1开始,删除2个元素。返回被删除的元素 var res = arr.splice(1, 2) // 从索引1开始,插入元素2,元素3 (插入后的数组内顺序与此处顺序一致) var res = arr.splice(1, 0, 2, 3) // 将索引 1 开始的 2 个元素替换为元素 2,3,4 共 3 个元素 var res = arr.splice(1, 2, 2,3,4) - 反转
arr.reverse() toString每个元素调用toString转字符串后,使用逗号拼接// 可使用隐式转换,与以下写法等效 [1,2,3] + '' [1,2,3].toString() // '1,2,3'- 降维(只拆解一个维度)
;[[1,2,3],[4,5,6],[7,8,9]].flat() // 得到[1,2,3,4,5,6,7,8,9] ;[[1,[2,3]], [4,5,6]].flat() // 得到[1,[2,3],4,5,6] - 遍历
for+ 索引for(let i=0; i < arr.length; i++){ arr[i] }forin获取下标/属性名(obj keys)for(const i in arr) { arr[i] }for of可遍历实现 iterable 接口的对象for(const item of arr) {item}
遍历相关高阶函数
以下高阶函数的参数包含函数,数组中每个元素都会传入参数中的函数内
需要注意的是,传入函数的函数体若为代码块,需要明确返回值,不然将默认返回 undefined ! 此处以第一个 some 为例详细说明,后续省略
some返回是否存在满足条件的数组元素;[1,2,3].some(item => item == 0) // 假 ;[1,2,3].some(item => item == 1) // 真 ,条件不是代码块,省略 return ;[1,2,3].some(item => {return item == 1} ) // 代码块,需要写 return ;[1,2,3].some(item => {item == 1} ) // 错误示范! 代码块没有 return 默认无返回值,结果恒定为 undefinedevery返回是否数组内每个元素都满足条件;[1,1,1].every(item => item == 1)->truefind返回数组中满足传入函数的第一个元素的值 , 没有则返回undefinedvar a = [{'id': 1, 'data': 1}, {'id': 2, 'data': 2}, {'id': 3, 'data': 3}] a.find(item => item.id == 1) // {'id': 1, 'data': 1} a.find(item => item.id == 4) // undefinedfilter数组中传入函数返回值为真的所有元素,组成一个新数组并返回;[1, 2, 1, 3, 1, 4].filter(item => item == 1)->[1,1,1]map已有数组元素进行相同处理,返回值创建新数组;[1,2,3].map(item => item*2)->[2, 4, 6]reduce归纳(数学归纳法) 第一轮值由前两项确定,后续结果由下一项与上一轮的值计算// 将第一个元素与第二个元素作为参数,得到返回值。下一轮中将上一轮的返回值作为参数1,当前元素作为参数2 // 出入值 // 第一轮 1,2 -> 3 // 第二轮 3(1+2) 3 -> 6 // 第三轮 6(3+3) 4 -> 10 ;[1, 2, 3, 4].reduce((item1, item2) => item1 + item2) // 输出: 10sort排序,默认按编码值排序;[1, 3, 200].sort() // 编码值排序导致 2 开头的 200,排在 3 前面。最终输出 [1, 200, 3] ;[1, 3, 200].sort((item1, item2) => item1 - item2) // 比较两个元素谁大的规则,回传一个 number 值,根据 >0 =0 <0 判断两个值的大小 // item1 是后面的项,item2 是前面的项。若返回值 > 0,保持后项在后,前项在前flatMap先处理元素,再降维;[[1,[2,3]], [4,5,6], 7].flatMap(item => item instanceof Number ? item * 2 : item) // 共三项,仅最后一项 7 是数字,需要 `*2` // [1,[2,3],4,5,6,14]
Tips: 字符串不能直接调用以上数组的方法。如何使用详见#类数组使用数组方法
展开符
var a = [3,4,5]
var b = [1,2, ...a, 6,7]
console.log(b)
var c = [...b] // 快速复制
console.log(c)
类数组
什么是类数组?访问元素时可使用方括号+下标访问
var obj = {0: 1, 1: null, 2: undefined}
var str = 'abc'
类数组使用数组方法
可使用以下几个方法,让类数组可以像数组一样操作
Array.from类数组的 map 方法。将类数组转换为数组,从而方便后续可以调用数组其他方法// 遍历 Array.from('aaa', c => c.charCodeAt()) // [97, 97, 97] // 与 ['a', 'a', 'a'].map(c => c.charCodeAt()) 的输出相同<方法>.call/apply(调用该方法的对象)通过call/apply,修改this指向,让指定对象调用<方法>。关于call/apply方法,详见 # call&apply// 通过 Array 原型定位数组方法 console.log(Array.prototype.join.call('abc')) // 通过一个空数组定位数组方法 console.log([].join.apply('abc')) // a,b,c- 展开符(必须非常类似 Array 的对象才能使用, str 与 intObj 不能使用)
function func(){ // arguments.forEach(i => console.log(i)) // 报错,不是Array [...arguments].forEach(i => console.log(i)) } func(1,2,3)
解构
- 通过下标一对一赋值
var [a, b, c] = [1, 2, 3, 4] console.log(a, b, c) // 1 2 3 - 下标 2 后的元素没有对应变量读取 var [a, b, c] = [1, 2] console.log(a, b, c) // 1 2 undefined - 下标 2 的内容在数组中不存在,得到 undefined - 默认值
var [a,b,c,d=4] = [1,2,3] console.log(a,b,c,d) - 将其余元素打包为数组,都赋值给某一个变量(使用展开符)
var [a, ...b] = [1,2,3] console.log(a, b) // 1 (2)[2, 3] - 函数参数解构
function func1([a, b, ...c]) { console.log(a, b, c) // 1 2 (5)[3, 4, 5, 6, 7] } func1([1,2,3,4,5,6,7]) - 交换两数
var a = 1, b = 2 ;[a, b] = [b, a] console.log(a, b)
数组作为函数参数
此时函数传入的为数组的引用。即函数内修改数组元素,在函数调用结束后会对该数组真实生效
作用域
JS 中的作用域说明
代码块
在介绍作用域之前,先了解代码块。以下是三种不同类型的代码块
- 普通代码块
- 可以在任何位置使用代码块。以下代码可以正常运行
{ console.log(1) } - 可以随意嵌套代码块。以下代码也可正常运行
{ { { { { console.log(2) } } } } }
- 可以在任何位置使用代码块。以下代码可以正常运行
- 语句代码块
for(i = 0; i < 10; i++) { // for语句的代码块 console.log(i) } - 函数代码块: 函数在声明时也使用了
{}。函数的块是块的特例(特殊性在其内部定义函数的作用域中体现)// 函数每次调用时,会开辟新的空间。因此 **函数内声明的变量** 每次都是全新的。多次调用函数时,变量互不影响 function test(input) { const a = input console.log(a) } test(1) // 创建 input,创建了 a 接收 input 的值,调用结束后销毁 test(2) // 创建了新的 input 与 a
变量作用域(声明关键字)
const与let声明的变量作用域在其所在的代码块中(代码块中定义,代码块结束时销毁)。如果没有在代码块中,则为全局作用域- 例1: 普通代码块
{ const a = 1; // 出了该定义语句所在块,则该声明失效 { const b = 2 ; // 内层块内有效 } // 此时销毁 b console.log(a) // console.log(b) // 这里会报错 } // 此时销毁 a // 本例中 由于该块内的子块也属于该块,所以内层块中 a 也有效 - 例2: 语句代码块
// i 在 for 语句内,且该语句含有代码, i 的作用域就是 for 语句的代码块 for(let i = 0; i < 10; i++) { //与别的 for 语句代码块中的 i 不冲突 console.log(i) // let 具有块级作用域。每次循环,都会在内存里产生一个全新的 i } // for 循环结束时销毁 i // console.log(i) // 这里会报错 - 例3: 代码块内不可重复定义
// 重复声明报错 let a = 1 let a = 2 // 报错:Identifier 'a' has already been declared
- 例1: 普通代码块
var定义变量特性
- 作用域至少为函数语句块
- 升格到作用域头部
- 可重复声明同一变量,会覆盖值
例1: 函数代码块
function testVar() { console.log(a) { var a = 1 console.log(a) } console.log(a) } // 由于 var 的作用域是 函数,所以 var 声明会跳出非函数的块。变成如下样子 /* function testVar() { var a // 1. 升格到函数作用域开始处 console.log(a) // 3. 还没赋值,输出 undefined { a = 1 // 2. 赋值保留原始位置 console.log(a) // 正常输出 } console.log(a) // a已经被赋值 正常输出 } */ // 最终输出 undefined 1 1例2: 普通块中的 var
console.log(a) // 1. 升格后,此处输出 undefined { console.log(a) // 2. 升格后,此处输出 undedefined var a = 1 // 0. 由于升格提升,该 var 为全局作用域。赋值仍然在此处 } console.log(a) // 3. 已赋值,此处输出 1 // 2.3 重复声明 var a = 1 var a = 'a' // 会覆盖之前的变量 // 即解释器会解析成如下内容 /* var a a = 1 // 此时该语句变成多余的赋值 a = 'a' */
- 代码块中无关键字的变量声明都是 全局变量
{ g1 = 1 { g2 = 2 } } console.log(g1, g2) // 普通代码块内声明的全局变量 // 1 2 for (i = 0; i<10; i++){ ; } console.log(i) // 语句代码块内声明的全局变量 // 10 function globalVar() { g3 = 3 } globalVar() // 需要执行一下函数以保证该全局变量的声明被执行 console.log(g3) // 函数代码块内声明的全局变量 // 3
关键字选择的Tips: 推荐默认使用 const 来声明所有变量,只有当您确定需要对变量进行重新赋值时(E.g. 在循环中,同一个变量 i 被多次赋值),才使用 let
后续的所有操作都是对这个对象内部结构的增、删、改,不改变其引用的,都可以使用 const
- 代码意图更清晰: 使用
const能明确地告诉其他开发者,这个变量的引用不会被改变,有助于提高代码的可读性和可维护性 - 减少意外错误: 它可以防止意外地对变量进行重新赋值,从而减少潜在的 bug
函数作用域
变量关键字形式声明的函数,其作用域符合变量作用域的逻辑
// E.g. var 定义函数,就是 var 变量,升格,但赋值位置不变 function func() { console.log(test) // 由于升格会输出 undefined var test = function () {} } func()function 关键字定义的函数,作用域类似 var,至少为函数代码块,且作用域升格到作用域头部。函数内容与
var不同,内容升格根据情况而定function 定义的函数在 普通代码块 和 语句代码块 中: 效果完全同 var
// E.g.1 不执行的语句代码块 console.log(func) // 升格效果同 var,未赋值,此处输出 undefined if (false) { // func 会升格,但该函数声明不被执行,因此 func 无法完成赋值 function func() {const b = 2} } console.log(func) // 同 var 变量,输出 undefined // E.g.2 执行的语句代码块 console.log(func1) // undefined if (true) { // func 会升格,该函数声明被执行了, if 的语句代码块升格效果同 var // 此处改用普通代码块使声明执行,也是相同升格效果,保持与 var 效果一致 function func1() {const b = 2} } console.log(func1) // 同 var 变量,输出 undefinedfunction 定义的函数在 函数代码块 中: 函数定义(赋值)也会升格
console.log(func) // 由于 func 在普通代码块,此处的升格会输出 undefined { function func() { console.log(test) // 由于 test 的定义在函数代码块,此时 test 的定义也会升格 // 正常输出 test 的定义 console.log(test()) // 由于定义也升格了,此处可在代码中定义的前方正常调用 test ,执行其中的 console.log(a) // 正常执行 test 内的代码,输出 1 function test() { const a = 1 console.log(a) } } } func()Tips: 在函数代码块这种特殊块中,
function定义的函数会完全升格,即test会把定义也提前。而一般的块中,升格效果同var一样,作用域升格但赋值不会升格,func在真正声明行之前是undefined
作用域链
观察以下示例代码
const x = 1
{ // block 1
const a = 1
{ // block 2
const a = 2
{ // block 3
const b = 3
for (i = 1; i < 10; i++){ // block 4
const c = 4
console.log(i, a, b, c, d, x, y)
}
}
}
}
说明: 当代码执行到 console.log(a, b, c, d, x, y) 时,解释器会先在其所在的作用域,即 block 4 中寻找对应变量。如果没有,则往父层作用域中寻找,依次向上,直至全局作用域,寻找目标变量。找到则停止该变量的查找,找不到则报错“未定义”
本例中几个特殊变量
- y: 一直往上无法找到。则报错“未定义”
- a: 由于依次往上,先找到值为 2 ,因此 a 会是 2。即使
a = 1对子层来说也是作用域,但a = 2会被先找到
此处依次向上查找的过程 block 4 -> block 3 -> block 2 -> block 1 -> 全局 就是作用域链
代码执行到任何位置都会有作用域链,此时可访问作用域链上的所有变量(同名时,内层会覆盖外层,例如本例中指定到最里边时的 a 变量)
函数
arguments函数的参数列表(类数组对象),仅非箭头函数可用function func() { console.log(arguments) // 不是真数组,会报错 // arguments.forEach(element => console.log(element)) // Array.from 将类数组迭代 Array.from(arguments, ele => console.log(ele)) // 使用 <func>.call 让指定对象调用 <func> 函数 ;[].forEach.call(arguments, element => console.log(element)) } func(1,2,3) // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] // 1 // 2 // 3 // 1 // 2 // 3- 可变参数(需配合展开符使用)
function func1(...arr) { // ... 展开符 console.log(arr) // arr 可变参数 console.log(arguments) } func1(1,2,3) // (3) [1, 2, 3] // Arguments(3) [1, 2, 3, callee: <accessor>, Symbol(Symbol.iterator): ƒ] // 与普通参数一起使用 function func2(p1, p2, ...parr) { console.log(parr) // 可变参数为 [3,4,5,6,7] console.log(arguments) // Arguments(7) [1, 2, 3, 4, 5, 6, 7, callee: (...), Symbol(Symbol.iterator): ƒ] } func2(1,2,3,4,5,6,7)
call&apply
观察以下代码
name = 'Alice'
function greet(msg1, msg2) {
console.log(this.name, "say: ", msg1, msg2)
}
const person = {
name: 'Bob'
}
// 直接调用时,greet 中的 this 指向全局作用域,读取到的 name 就是全局变量 name
greet('Hi.', 'Hello.')
// Alice say: Hi. Hello.
Function 原型中存在两个函数 Function.prototype.call 与 Function.prototype.apply,用于进行动态调用函数(调用对象或参数是需要动态变化的)
使用方法为 <func>.call/apply(参数),以下是参数说明
第一个参数
thisArg: 为this指向,即调用目标函数的对象。<func>.call/apply(obj)即为obj.<func>()如果函数不处于严格模式,当第一个参数为null或undefined时,会被替换为全局对象,原始值会被转换为对象后续参数有所不同
call目标函数的参数作为 call 的后续参数call(thisArg, arg1, arg2, /* …, */ argN)greet.call(person, 'Hi.', 'Hello.') // Bob say: Hi. Hello.apply目标函数的参数以数组方式传入apply(thisArg, argsArray)greet.apply(person, ['Hi.', 'Hello.']) // Bob say: Hi. Hello.call配合数组展开符greet.call(person, ...['Hi.', 'Hello.']) // Bob say: Hi. Hello.
高阶函数
如果一个函数的参数或返回值是一个函数。则该函数称为高阶函数
例如:
数组的多个遍历相关函数
map,some,every... 其参数包含一个函数 #数组遍历相关高阶函数闭包时,函数的返回值就是高阶函数
匿名函数
没有函数名的函数表达式
形如 function(){} 或 () => {}。下面对匿名函数常见用法进行说明
赋值给变量
通过 const func1 = function() {} 将函数赋值给变量
赋值给对象的属性(this指向说明)
此时该匿名函数就是对象的方法
const obj1 = {name: 'obj1'}
obj1.getName = function(){return this.name}
Tips: 普通对象的方法最好不要使用箭头函数(构造函数实例化时不会存在问题),会出现 this 指向问题。箭头函数中的 this 指向箭头函数声明时所在的作用域
对象本身不会创建作用域名,即使对象嵌套,依然指向最外层对象所在作用域(对象的属性是一个对象,该内层对象的方法使用箭头函数读取 this 时,依然指向外层函数的作用域)
obj1.getName = () => {return this.name}
obj1 = {
name: 'obj1',
getName: () => {return this.name}
}
// 以上两种对象属性是箭头函数的写法,等效为以下两行代码
const getName = () => this.name; // 此时由于先定义了函数,this 指向该箭头函数声明行所在作用域
obj1.getName = getName; // 再将该函数绑定到对象上。此代码中,函数内的 this 依然保持指向全局
以下是更为复杂的情况(箭头函数绑定的对象的作用域不是全局)。首先定位箭头函数的声明
var name = 'glb'
function func1() {
var name = 'func1'
var obj1 = {name: 'obj1'}
function func2() {
var name = 'func2' // 1. 局部变量,不会挂载到 func2 的 this 上。除非编写 this.name
obj1.getName = () => {return this.name} // 0. 此时 this 是 func2 的 this
}
func2() // 2. 当一个普通函数被独立调用(没有上下文)时,它内部的 this 默认指向全局对象。因此 func2 的 this 就是全局
console.log(obj1.getName())
// 输出 glb
}
func1()
// () => {} 箭头函数声明所处作用域的 this 就是箭头函数的 this
关于 this:
- 一般情况(非严格模式)下,
this始终指向全局。如函数的独立调用。(严格模式,this指向undefined) - 隐式绑定: 当非箭头函数有上下文对象时,如
obj.func(),函数内的this指向该对象(箭头函数除外,其始终指向所在行的作用域) - 隐式丢失
- 使用另一个变量来给函数取别名
var func1 = obj.func // func1 会丢失 this 指向。使用 `func1()` 时,内部的this恢复指向全局(因为调用时,调用者是全局) // func 作为 obj 的方法,`obj.func()` 依然保持 this 指向 obj obj1.func = obj.func // 别名,发生隐式丢失 obj1.func() // 发生隐式丢失,此时调用者是 obj1,因此 this 指向 obj1 - 将函数作用参数传递时会被隐式赋值,回调函数丢失 this 绑定,转移到指向全局
function callback(func) { func() } obj = {num: 0, func(){ console.log(this.num) }} num = 1 obj.func() // 0 // 根据上下文,this 指向为 obj callback(obj.func) // 作为参数,发生隐式丢失
- 使用另一个变量来给函数取别名
- 显示绑定: 使用
callapplybind(箭头函数无法使用这些方法修改this指向) new绑定。通过new创建的对象,构造时绑定的方法内的this指向该对象num = 0 obj = { num: 1, func() { console.log(this.num) return ()=>{console.log(this.num)} } } obj.func() // 1 // func 中的 this 指向 obj obj.func()() // 1 // 先执行 func 中的打印,输出 obj 的 num // 1 // 返回的箭头函数被执行,其 this 等同于定义行作用域的 this。 // 由于定义在 func 中,且 func 中的 this 指向 obj。故 箭头函数的 this 也指向 obj
作为函数的参数(类似回调)
setTimeout(() => {
console.log(1)
}, 1000)
作为函数返回值(闭包)
在 js 中,某些变量需要全局访问,但又不想被轻易修改。
因此采用函数嵌套,使变量的作用域在一个函数中,内层函数对这个变量进行引用与修改。再通过一个变量接收内层函数,当需要此变量时,直接调用内层函数
function createCounter() {
let count = 0; // 局部变量
// 返回一个内部函数(匿名函数)
return function() {
count++;
console.log(count);
};
}
const myCounter = createCounter(); // 使用一个变量接收内层的匿名函数
myCounter(); // 输出 1
myCounter(); // 输出 2
myCounter(); // 输出 3
在 python 中,也能实现相同效果。但 python 的内层函数不能直接通过作用域链访问外层函数的变量与全局变量,需要使用关键字 nonlocal 与 global
自执行函数(分隔命名空间)
多个人员同时编辑一个文件时,在 ES6 前由于作用域仅为 全局作用域 和 函数作用域
为了防止全局作用域下的标识符重名,使用匿名函数使自定义标识符作用域仅在自定义函数内
以下以 变量 a 在三种命名空间为例,说明自执行函数的三种写法
!function(input){
var a = input
}(1) // 使用 `!` `+` `-` `~`
;(function(input){ // 括号开头使用 ;
var a = input
}('a')) // 参数在括号内
;(function(input){
var a = input
})({}) // 参数在括号外
ES6 可使用普通代码块 与 const / let 关键字进行标识符隔离
对象
key 只能为字符串或符号
字面量声明
const prop = 'name'
const prop1 = 'a'
const prop2 = 'ge'
const obj2 = {[prop]: 'obj2', [prop1 + prop2]: 10}
console.log(obj2.name, obj2.age)
属性
对象的属性可以是 Symbol,关于 Symbol 详见 #内置对象 Symbol
- 属性遍历
const readPropsObj = {a: 1, b: 2, c: 3, [Symbol()]: 4} // 方法1: 获取属性数组,Symbol属性 和 描述符为不可枚举的 无法获取 console.log(Object.keys(readPropsObj)) // 方法2: for key in obj, Symbol属性 和 描述符为不可枚举的 无法获取 for(let key in readPropsObj) { console.log(key, readPropsObj[key]) } // 方法3: 获取 Symbol 属性的数组 - 此方法只输出对象的 Symbol,不包含普通属性 console.log(Object.getOwnPropertySymbols(readPropsObj)) // [Symbol()] - 查询是否含有指定属性
const obj1 = {name: 1} // 字面量方式声明对象 Object() 构造函数声明对象 console.log('name' in obj1, 'name1' in obj1)
封包与解构
ES6 语法
- 封包
// 根据变量快速创建对象 a = 1 b = 2 c = 3 d = {a, b, c} console.log(d) // {a:1, b:2, c:3} - 解构
//根据对象快速声明变量 - 对象快速解构 e = {f:1} const {f} = e // `f = e.f = 1` 将对象属性 f 声明为一个同名变量 console.log(f) // 1 const {f:ff} = e // `ff = e.f = 1` 将对象属性 f 的值赋值给变量 ff console.log(ff) // 1 // 声明与解构不在同一行时需要括号 let g ({g} = {g:1}) // 先声明再解构时,需要使用小括号,将解构处括起。防止解释器解释成对象赋值给代码块 console.log(g) // 1 // 解构默认值 const {aa: aaa = 100} = {a: 1} // 不存在时使用默认值 console.log(aaa) // 100
对象方法
const obj3 = {}
// 写法1 - 函数对象
function test (){}
obj3.test = test
// 写法2 - 匿名函数
obj3.test = function() {}
// 写法3 - 箭头函数
//(注意此写法会产生之前在匿名函数处提到的this指向问题)
obj3.test = () => {}
// 写法4 - 声明时的匿名函数简写
const obj4 = {
test() { // 等效为 test: function() {}
console.log(1)
}
}
console.log(obj4)
// 写法5 - 使用封包
const obj5 = {test} // 等效为 {test: test}
console.log(obj5)
展开符
var a = {a:1}
var b = {...a, b:2}
console.log(b)
var c = {...b} // 快速复制
console.log(c)
Object部分方法
obj = {a: 1, b: 2}
Object.entries(obj) // 键值对二维数组
// [['a', 1], ['b', 2]]
Object.keys(obj) // 键数组
// ['a', 'b']
Object.values(obj) // 值数组
// [1, 2]
arr = Object.entries(obj)
Object.fromEntries(arr) // 二维数组/Map转对象
// {a: 1, b: 2}
作为函数参数
与数组类似,此时函数传入的为对象的引用。即函数内修改键值对,在函数调用结束后会对该对象真实生效
类
在 ES6 中新增了通过 class 关键字创建类的方式
class ES6Class {
// class 关键代码块内的代码,默认都是严格模式
// 实例属性,只能通过实例访问,每个实例独立拥有
attr // 无初始值
attrInt = 10 // 有初始值
// 静态属性/类属性,只能通过类访问
static clsAttr // 无初始值
static clsAttrInt = 20 // 有初始值
// 实例方法 - this 指向对象
func = function(){console.log(this.attrInt)}
// 可简写为
// func() {console.log(this.attrInt)}
// 使用箭头函数
// func = () => {console.log(this.attrInt)} // 此处 this 指向不会混乱
// JS 引擎底层,箭头函数被等效转换到 constructor(构造函数)里面去执行的。constructor 内部的 this 就是正在被 new 出来的那个实例对象本身
// 类方法 - this 指向类
static clsFunc = function(){console.log(this.clsAttrInt)}
}
obj1 = new ES6Class()
// 实例属性
console.log(obj1.attrInt)
console.log(obj1.clsAttr) // 类属性,对象无法访问 undefined
// 访问 与 修改 类属性
ES6Class.clsAttr = 15 // 无初始值的属性需要手动赋值
console.log(ES6Class.clsAttr)
console.log(ES6Class.attrInt) // 实例属性,类无法访问 undefined
// 实例方法
obj1.func()
// ES6Class.func() // 实例方法,类无法访问 TypeError: ES6Class.func is not a function
// 类方法
ES6Class.clsFunc()
Tips: 实例方法声明时,普通方法声明与箭头函数声明有不同点。假设类存在普通方式声明的实例方法 normalFunc 与箭头函数声明的 arrowFunc
- 箭头函数的优势: 当需要把方法提取出来单独调用(比如作为事件回调)
const testNormal = obj.normalFunc普通函数的 this 指向取决于执行。独立调用时 this 本该是 window,但 class 内部默认严格模式,this 变成了 undefined。如果其中存在this调用,会出现混乱const testArrow = obj.arrowFunc箭头函数的 this 指向取决于定义位置。它诞生在 constructor 里,this 早就永久绑定为 obj 实例了
- 箭头函数的缺点
- 使用普通方法 normalFunc 是挂载在类的原型(prototype)上的。如果 new 了 1000 个实例,内存里其实只有 1 个 normalFunc 函数,大家共享,非常节省内存。
- 箭头函数 arrowFunc = () => {}:它是作为属性直接挂载在实例上的。因为每次 new 实例化时,constructor 都会重新运行一次,所以如果 new 了 1000 个实例,内存里就会创建 1000 个长得一模一样的箭头函数,比较占用内存
构造函数
在初始化时自动调用,可以给属性赋值
class ClassConstru {
// 由于构造方法进行了属性绑定,此处的实例属性声明可以省略
attr1
attr2
// 构造函数 - this指向对象
constructor(p1, p2) { // 实例化时的形参
this.attr1 = p1
this.attr2 = p2
// 构造函数不应有返回值
// 返回一般值无效,例如 int
// 返回对象时,构造出来的对象就是返回的对象,导致类的声明无意义
}
}
obj2 = new ClassConstru('1') // 在实例化时会自动执行 class 内的 constructor
console.log(obj2)
// ClassConstru {attr1: '1', attr2: undefined}
封装
class PrivAttrCls {
// 私有属性使用 # 声明
#privAttr
// 构造时修改私有属性
constructor(p) {this.#privAttr = p}
// 获取方法 - 私有属性只能通过定义实例方法进行获取
getPrivAttr() {return this.#privAttr}
// 修改方法 - 私有属性只能通过定义 实例方法 或 构造方法 进行修改
setPrivAttr(p) {this.#privAttr = p}
}
obj3 = new PrivAttrCls('p') // 私有属性赋值
console.log(obj3) // PrivAttrCls {#privAttr: 'p'}
// console.log(obj3.#privAttr) // 报错 Private field '#privAttr' must be declared in an enclosing class
obj3.setPrivAttr('priv') // 通过 实例方法 或 构造方法 才能修改私有属性
console.log(obj3.getPrivAttr()) // 通过实例方法才能访问私有属性
// 可以不提供 set 方法,来实现属性是只读的效果
// 为了使私有属性可以通过 <class>.<attr> 方式访问与修改,在声明时将私有属性与 `get` `set` 关键字一起使用
class PrivAttr {
#privAttr
get privAttr() {return this.#privAttr}
set privAttr(p) {this.#privAttr = p}
}
obj4 = new PrivAttr()
obj4.privAttr = 25 // 调用 `set privAttr` 方法
console.log(obj4.privAttr) // 调用 `get privAttr` 方法
// 25
继承与重写
class Parent {
#attr
get attr() {return this.#attr}
set attr(p) {this.#attr = p}
constructor(p) {this.attr = p}
}
class Child extends Parent {
// 重写构造方法
constructor(p){
// 重写构造方法,需要保持参数一致,并调用父类的构造方法
super(p)
this.attr = p + '1'
}
}
const child = new Child(1)
console.log(child) // 继承并重写父类构造方法
// Child {#attr: '11'}
child instanceof Parent // 对象是否为目标类或其子类
// true
可选链
判断内容是否存在
class Test {
func() {console.log('call func()')}
}
const test = new Test()
test && test.func && test.func() // 判断对象是否存在,对象的对应属性是否存在
test?.func?.() // 方法的可选链
拷贝
- 同一指向,不是复制
var arr1 = [1,2,3,4,5] // 都是引用,指向同一个对象 var arr2 = arr1 arr1[0] = 6 // 因为指向同一个对象,因此通过一个引用修改,另一个也会被影响 console.log(arr2[0]) // 6
浅拷贝
得到两个不同的数组/对象,但数组/对象内的非基本类型元素还是同一引用
- 使用展开符进行浅 copy
- 数组
var arr1 = [0, {x:1}] var arr2 = [...arr1] arr1[0] = 1 arr1[1]['x'] = 2 // arr2 的第 0 项不是引用,不受影响 console.log(arr2) // [0, {x: 2}] - 对象
var obj1 = {name: 'Jacky', age: 35, data: {x:1}} var obj2 = {...obj1} obj1.age = 36 obj1.data.x = 2 console.log(obj2.age, obj2.data) // 35 {x: 2}
- 数组
- 循环赋值
- 数组
var arr1 = [{x:1}] var arr2 = [] arr1.forEach(i => arr2.push(i)) arr1[0]['x'] = 2 console.log(arr2[0]) // {x: 2} - 对象
var obj1 = {name: 'Jacky', age: 35, data: {x:1}} var obj2 = {} for (const k in obj1) { obj2[k] = obj1[k] } obj1.data.x = 2 console.log(obj2.data) // {x: 2}
- 数组
- 使用对应的修改方法
- 数组
slicevar arr1 = [{x:1}] var arr2 = arr1.slice() arr1[0]['x'] = 2 console.log(arr2[0]) - 对象
assignvar obj1 = {name: 'Jacky', age: 35, data: {x:1}} var obj2 = Object.assign({}, obj1) obj1.data.x = 2 console.log(obj2.data)
- 数组
深拷贝
- 浏览器方法
structuredClone(node环境没有,会报错)var obj1 = {name: 'Jacky', age: 35, data: {x:1}} var obj2 = structuredClone(obj1) obj1.data.x = 2 console.log(obj2.data) // {x: 1} - 使用
JSON,会丢失方法与一些特殊内容var obj1 = {name: 'Jacky', age: 35, data: {x:1}} var obj2 = JSON.parse(JSON.stringify(obj1)) obj1.data.x = 2 console.log(obj2.data) // {x: 1}
内置对象
解释器自带对象分为两种
- 内置对象: ES标准定义的对象,如 Date Math Array Object 等
- 宿主对象: 浏览器提供的对象,如 window navigator document 等
以下是部分 JavaScript 标准内置对象
Symbol
用于创建唯一标识
每次调用,都会产生唯一标识
const s = Symbol()
console.log(s) // Symbol()
console.log(typeof s) // symbol
作为对象属性名,可以防止重名
且使用 symbol 作为属性名,该属性无法通过 for 遍历(无法得知和使用该属性)
const obj1 = {} // 字面量方式创建一个对象
const s1 = Symbol()
obj1[s1] = 's1'
console.log(obj1, obj1[s1])
// 如果没有创建时返回的变量 s1,则无法访问
// 例如浏览器内置对象类似的预先生成对象,无法获取属性名的symbol,则无法访问该属性的值
const obj2 = Object() // Object方法返回一个新对象
obj2[Symbol()] = 'cannot access' // 预先定义的symbol
// 此时无法获取之前 symbol 变量,而新创建的 symbol 是全新值,因此输出 undefined
console.log(obj2, obj2[Symbol()]) // 此时无法获取之前 symbol 变量,而新创建的 symbol 是全新值,因此输出 undefined
console.log(Object.keys(obj2)) // Symbol属性无法正常遍历,此处返回空数组
// 可通过获取对象 symbol 获取到当时声明的 symbol对象,从而获取属性值
obj2[Object.getOwnPropertySymbols(obj2)[0]] // 'cannot access'
Date
用于对时间进行获取与处理。以下是获取时间戳的几种写法
new Date().getTime()
;(new Date).getTime()
Date.now()
;+new Date() // 隐式转换
Map
普通对象(key 字符串 或 符号)的扩展, key 可以是任意值
var mapping = new Map()
// 必须通过 set 增加键值对
var temp = {}
mapping.set(temp, 'xxx') // 此处键是一个空对象。直接写空对象,取值时无法得到该空对象的引用
mapping.set(undefined, 1)
console.log(mapping)
var temp1 = temp
// 获取必须通过 get
console.log(mapping.get(temp1)) // 当 key 为对象/数组等复合类型时,需保证传入引用指向相同地址
// 删除必须通过 delete
console.log(mapping.delete(temp1))
// 清空键值对
mapping.clear()
// 键值对个数
console.log(mapping.size)
// 是否包含指定键值对
mapping.has(undefined)
console.log(mapping)
// 转二维数组
var mappingArr = Array.from(mapping)
var mappingArr = [...mapping]
// 使用二维数组在构建 Map 时给予初始值
var mapping = new Map([[1,2], [true, false]])
console.log(mapping)
// 遍历
// 1. for - of Map 自身已经实现了 iterator 接口
for(const ele of mapping) {
console.log(ele) // 键值对数组
}
// 2. for of mapping.entries()键值对 / mapping.values()值 / mapping.keys()键
// 3. forEach
mapping.forEach((value, key) => console.log(key, value))
Set
集合。一种特殊的 Map,使用 Map 实现的不能重复
var set = new Set()
// 添加元素
set.add(1)
// 删除
set.delete(1)
// 是否包含
set.has(1)
// 数量
set.size
// 清空
set.clear()
// set转数组
var arr = [...set]
// 数组转set
var set = new Set(arr)
// 数组去重
var arr = [...new Set(arr)]
// 遍历
for(const ele of set) {
console.log(ele)
}
for(const ele of set.entries()) { // Set 是特殊的 Map,键值相同,利用 Map 键不能重复实现去重
console.log(ele)
}
String
部分方法同 Array。由于字符串是基本类型(不可变),修改内容时不会修改原始字符串对象,而是返回一个新字符串
Tips: Class.prototype.func 表示该方法调用时需要对象,不是静态方法
var str = new String('111') // 字符串对象
var str = 'Hello' // 原始类型字符串 ,与字符串对象等效
str.length // 长度
str.charAt(2) === str[2] // 获取指定索引的字符
// 拼接,不修改原始字符串
console.log(str.concat(' ', 'world!'), str)
// 开头结尾
console.log(str.startsWith('H'), str.endsWith('o'))
// 搜索
console.log(str.indexOf('l'), str.lastIndexOf('l'))
// 是否包含字符
console.log(str.includes('z'))
// 填充 - X => 00X - 不修改原始
console.log('1'.padStart(3, '0'), '1'.padEnd(3, '0'))
// 重复
console.log('='.repeat(10))
// 切分
console.log(str.slice(1, 3)) // 开始索引与结束索引
console.log(str.substring(1, 3)) // 开始索引与结束索引
console.log(str.substr(1, 2)) // 开始索引与切分长度
// 修改
console.log(str.splice(1, 0)) // 指定索引处,移除指定数量的字符
// 大小写
console.log(str.toLowerCase(), str.toUpperCase())
// 移除前后空 - python中的 strip()
console.log(str.trim(), str.trimStart(), str.trimEnd())
// 编码
// String.prototype.charCodeAt() - 获取字符的 ascii 码
console.log(Array.from('abc', c => c.charCodeAt(c)))
// String.fromCharCode() - ascii码转字符
console.log(String.fromCharCode(65,66,67,68))
RegExp
Regular expression,检索或替换指定规则的文本
// 构造表达式声明正则时,匹配内容为字符串,因此需要编写转义字符,参数 2 为模式, i 表示不区分大小写 - 字符串可进行混淆与加密
var reg = new RegExp('\\d+', 'i')
// 隐式创建,字面量创建
// 此时正则字符串不需要转义,用两个 / 包裹,后跟模式 - 不能进行混淆加密
var reg = /\d+/i
console.log(reg.test('11aa22bb33cc'))
// 是否匹配到目标格式字符串,是否包含
console.log(reg.exec('11aa22bb33cc'))
// 匹配出符合规则的内容(只返回第一项)
// ['11', index: 0, input: '11aa22bb33cc', groups: undefined]
console.log(reg.exec('aabbcc'))
// 匹配不到符合规则的内容返回 null
匹配模式/修饰符
i忽略大小写g全局匹配(多次匹配),正则对象会初始化一个计数器,每次匹配时会增加次数m多行匹配sdotAll(点匹配全部)模式,.可以匹配换行符
var reg = /\d+/g
console.log(reg.test('11aa22bb33cc'))
// 匹配出第 1 项符合条件的 '11' - 返回 true
console.log(reg.exec('11aa22bb33cc'))
// 匹配出第 2 项符合条件的 '22' - 返回 ['22' ....]
console.log(reg.exec('11aa22bb33cc'))
// 匹配出第 3 项符合条件的 '33' - 返回 ['33' ....]
console.log(reg.exec('11aa22bb33cc'))
// 匹配不到第 4 项符合条件的 - 返回 null
// 一般通过循环,条件为返回值是否为null,得到所有满足条件的子字符串
// while(const res = reg.exec(str)) {}
console.log(reg.exec('11aa22bb33cc'))
// 循环到第 1 项符合条件的 '11' - 返回 ['11' ....]
量词
符号量词
+1次到多次*0次到多次?0次或1次
指定次数
{0}指定字符出现指定次数(此处表示被修饰的内容没有出现){0,1}指定字符出现0-1次(等效为'?'){1,}指定次数出现1次异常(等效为'+')
惰性匹配,尽量少匹配字符,在 量词后增加
?
console.log(/a.*b/.exec('a1b2c3b'))
// 默认是贪婪匹配 - 整个字符串匹配后,一个个往外吐出字符
console.log(/a.*?b/.exec('a1b2c3b'))
// ^ 只匹配指定规则开头的字符串 $ 只匹配行末
var str = 'ab1ab2ab3b'
let res = null
var reg = /ab/g
var reg = /^ab/g
while (res = reg.exec(str)) {
// 不要把正则直接写在while中,不然每次会生成新的正则对象,导致正则对象的计数器也是新的,从而死循环
console.log(res)
}
元字符
.除换行符外的任意字符(s模式可以匹配全部)[a-d]字符范围 a-d,[xyz]字符范围 x/y/z ,[^xyz]反选,不是x/y/z的字符串(中括号内表示反选,中括号外表示行首)\w字母数字下划线(等效[a-zA-Z0-9_])\W不包含字母数字下划线(\w的反选)\d数字(等效[0-9])\D非数字(\d的反选,等效[^0-9])\s空白字符\S非空字符[\w\W][\s\S]... 等表示匹配所有字符。类似 s 模式下的.console.log(/[a-c]+/.exec('1acc122bcd33asd11aaa')) // ['acc', index: 1, input: '1acc122bcd33asd11aaa', groups: undefined]\b单词边界\B非边界console.log(/h.+o\b/.exec('hxoob hi hello ooo aluoha')) // 匹配出 'hxoob hi hello ooo'。 h 开头 o 结尾,o后方是边界 的 贪婪匹配 console.log(/h.+?o\b/.exec('hxoob hi hello ooo aluoha')) // 匹配出 'hxoob hi hello' h 开头 o 结尾,o后方是边界 的 惰性匹配 console.log(/h.+?o\B/.exec('hxoob hi hello ooo aluoha')) // 匹配出 'hxo'。 h 开头 o 结尾,o后方不是边界\匹配特殊字符的转义符console.log(/\.+/.exec('xxx...xxx...xxx')) // 将 \ 修饰的 '.' 作为一个字符进行匹配
分组
可用于匹配出子表达式
|或,匹配某一种规则console.log(/Hello|world/.exec('Hello JS!')) // `|` 前方都是条件1,后方都是条件2 console.log(/Hello|world/.exec('Hi world!')) // 使用括号限制条件长度 - 同时返回值多了分组内容(第二项) console.log(/a(b|c)z/.exec('zzzabzzz')) // ['abz', 'b', index: 3, input: 'zzzabzzz', groups: undefined] - 子表达式 console.log(/a(b|c)z/.exec('zzzaczzz'))- 是否捕获。使用分组时默认捕获返回值,通过
?:使分组不捕获返回值console.log(/a(b|c)z/.exec('zzzaczzz')) // 返回值第二项由index变为分组内容 // ['acz', 'c', index: 3, input: 'zzzaczzz', groups: undefined] console.log(/a(?:b|c)z/.exec('zzzaczzz')) // 在分组前增加 ?: ,使分组内容不进行返回,此时返回内容的第二项依然是index // ['acz', index: 3, input: 'zzzaczzz', groups: undefined] - 反向引用: 引用前一个子表达式捕获到的结果
// 匹配前两个字符与后两个字符相同,中间为任意字符的长度为5的字符(形如AAxAA) console.log(/(.{2}).\1/igs.exec('aacaabaabbcacc')) // 此处的 `\1` 为占位符,值为第一个子表达的结果 // ['aacaa', 'aa', index: 0, input: 'aacaabaabbcacc', groups: undefined] - 零宽断言: 匹配指定条件开头的字符串(条件不占用宽度,不作为最后的匹配结果),使用
?开头- 分类
- 先行/后行:决定了匹配内容在前还是后(有
<表示后行)。 - 正向/反向:决定了期望的结果(必须有还是必须没有)(
=/!)。
- 先行/后行:决定了匹配内容在前还是后(有
<=正向后行 E.g. 获取数字开头的 3 个连续字母(正向: 需要符合条件, 后行: 要匹配的目标在条件后面)var str = 'axyz1abcb0xZy2xyz' var reg = /(?<=\d)[a-z]{3}/igs // ? + <= var ret = null while(ret = reg.exec(str)) { console.log(ret) } // ['abc', index: 5, input: 'axyz1abcb0xZy2xyz', groups: undefined] // ['xZy', index: 10, input: 'axyz1abcb0xZy2xyz', groups: undefined] // ['xyz', index: 14, input: 'axyz1abcb0xZy2xyz', groups: undefined]<!反向后行 E.g. 获取非数字开头的 3 个连续字母console.log(/(?<!\d)[a-z]{3}/igs.exec('1abc2abcyabc3abc')) // ['bcy', index: 5, input: 'axyz1abcb0xZy2xyz', groups: undefined]=正向先行 E.g. 获取数字结尾的 3 个连续字母console.log(/[a-z]{3}(?=\d)/igs.exec('1abcyabc2abczabc')) // ['abc', index: 5, input: 'axyz1abcb0xZy2xyz', groups: undefined]!反向先行 E.g. 获取非数字结尾的 3 个连续字母console.log(/[a-z]{3}(?!\d)/igs.exec('1abc1abc2abc#abc')) // ['abc', index: 9, input: '1abc1abc2abc#abc', groups: undefined]
- 分类
?<>分组命名(?<分组名>匹配符)。将捕获到的内容放入groups中,方便获取console.log(/<a href="(?<url>.*?)">(?<text>.*?)<\/a>/.exec('<a href="http://www.google.com">Google</a>').groups) // groups: {url: 'http://www.google.com', text: 'Google'}
匹配html
var htmlStr = `
<p>
<div>
<a href="http://www.google.com">Google</a>
<a href="http://www.bing.com">Bing</a>
<a href="http://www.baidu.com">百度</a>
</div>
xxx
<span>
`
// 构造目标格式的第一项,记得全局模式,不然后续会死循环
var reg = /<a href="http:\/\/www.google.com">Google<\/a>/g
// 将 url 内容替换为 \w 和 \.(注意'.'的转义) 标签内容可能是中文,使用 '.' 进行匹配(.*? - 常用的万能匹配,注意s模式)
var reg = /<a href="http:\/\/\w+\.\w+\.\w+">.+?<\/a>/g
// 匹配标签的属性时,常常使用 [^"]*? 排除引号的惰性匹配获取引号内的属性内容。[\s\S]*? 用于在非s模式下,出现换行符等内容的万能匹配
var reg = /<a href="http:\/\/[^"]*?">[\s\S]*?<\/a>/g
// 通过分组获取具体内容
var reg = /<a href="(http:\/\/[^"]*?)">([\s\S]*?)<\/a>/g
var retval = null
while(retval = reg.exec(htmlStr)) {
console.log(retval)
}
// (3) ['<a href="http://www.google.com">Google</a>', 'http://www.google.com', 'Google', index: 27, input: '\n <p>\n <div>\n <a href="http://www.goo….baidu.com">百度</a>\n </div>\n xxx\n <span>\n', groups: undefined]
// (3) ['<a href="http://www.bing.com">Bing</a>', 'http://www.bing.com', 'Bing', index: 78, input: '原始字符串,同上,此处省略', groups: undefined]
// (3) ['<a href="http://www.baidu.com">百度</a>', 'http://www.baidu.com', '百度', index: 125, input:'原始字符串,同上,此处省略', groups: undefined]
正则替换
replace支持字符串与正则console.log('13911112222'.replace('1111', '****')) console.log('13911112222'.replace(/(?<=1[3-9]\d+)\d{4}/, '****')) // 139****2222replaceAll// replace + 全局模式下,会将所有匹配项都进行替换 console.log('aaxxccx'.replace(/x/g, '*')) // replaceAll 等效为 replace + 全局模式 console.log('aaxxccx'.replaceAll('x', '*')) // aa**cc*match获取匹配结果// 非全局模式,返回内容等效为 RegExp.prototype.exec 第一次的结果 console.log('aaxxaacc'.match(/aa/)) // ['aa', index: 0, input: 'aaxxaacc', groups: undefined] // 全局模式,直接返回匹配到的所有字符串组成的的数组(无 index,groups 等信息) console.log('aaxxaacc'.match(/aa/g)) // ['aa', 'aa']matchAll返回可迭代对象(后面的正则必须全局模式)console.log('aaxxaacc'.matchAll(/aa/g)) // RegExpStringIterator {} // 使用数组解包将迭代对象解包 // 得到 RegExp.prototype.exec 全部结果组成的数组(因为exec结果本身为数组,最终是二维数组) console.log([...'aaxxaacc'.matchAll(/aa/g)]) // [ // ['aa', index: 0, input: 'aaxxaacc', groups: undefined], // ['aa', index: 4, input: 'aaxxaacc', groups: undefined] // ]search获取匹配内容的索引console.log('abc123'.search(/\d/)) // 3split字符串切分,支持字符串与正则console.log('123a321b123c321'.split('b')) // ['123a321', '123c321'] console.log('123a321b123c321'.split(/[a-z]/)) // ['123', '321', '123', '321']
包装类
对象才能调用方法与属性,但以下原始类型也能调用方法与属性
String,Number,BigInt,Boolean 等,在需要的时候,解释器会对这些原始类型包装成对象,从而调用对应方法
该过程是解释器自己完成,无需手动操作
''.length
// 0
;''.charCodeAt()
// NaN
Tips: null 与 undefined 没有包装类,没有方法可以调用
Function
函数的构造方法。自定义函数所属的类 func.__proto__ === Function.prototype
- 使用
Function创建函数func = new Function('a', 'return a') // 参数为入参与函数体 // 等效为以下内容 // function func(a) {return a} - 构造器过
debugger: 已知func.constructor = func.__proto__.constructor === Function.prototype.constructor === Function// 将构造器置空 Function.prototype.constructor = function(){} // 再此之后声明的函数 function func(params) {return params} // 其构造器就是空函数 func.constructor // 输出 ƒ (){}- Function 构造 debugger: 以下是单次的 debugger。通过与定时器配合实现反复触发 debugger 但不影响主进程执行业务逻辑
// 通过自定义函数构造器,调用 Function 构造 debugger 并调用 func.constructor('debugger').call() // 通过匿名函数 (function(){}.constructor('debugger').call()) - 方法
- 重写定时器
_setInterval = setInterval setInterval = function(a, b){ // 判断无 debugger 关键字,再正常设置定时器 if (a.toSting().indexOf('debugger') === -1) { return _setInterval(a, b) } } - 重写构造器
_constructor = Function.prototype.constructor Function.prototype.constructor = function() { if(arguments[0] === 'debugger') { // 什么都不做 } else { return _constructor.apply(this, arguments) } } - 重写代码: 在发生debugger时查看堆栈,定位触发debugger的代码(函数),在执行代码块前下断点。刷新后重写该代码,再放行
- 重写定时器
- Function 构造 debugger: 以下是单次的 debugger。通过与定时器配合实现反复触发 debugger 但不影响主进程执行业务逻辑
全局函数
- url编解码 (注意,根据所用函数不同,编码时仅对query参数内容进行编码)
- 编码
// encodeURIComponent - 此函数只能对参数内容进行编码 console.log(encodeURIComponent('params?a=[1]&b=测试')) // 此写法 `?` `=` `&` 也会被编码 // params%3Fa%3D%5B1%5D%26b%3D%E6%B5%8B%E8%AF%95 console.log(`params?a=${}&b=${encodeURIComponent('测试')}`) // 正确写法encodeURIComponent('[1]') // params?a=%5B1%5D&b=%E6%B5%8B%E8%AF%95 // encodeURI - 此函数可以只对参数进行编码 console.log(encodeURI('params?a=[1]&b=测试')) // params?a=%5B1%5D&b=%E6%B5%8B%E8%AF%95 - 解码
console.log(decodeURIComponent(decodeURI('params?a=%5B1%5D&b=%E6%B5%8B%E8%AF%95'))) console.log(decodeURI('params?a=%5B1%5D&b=%E6%B5%8B%E8%AF%95'))
- 编码
- unicode编解码
console.log(escape('测试')) // %u6D4B%u8BD5 console.log(unescape('%21')) - str2int(也可通过
Number类进行调用)console.log(parseInt('1')) console.log(Number.parseInt('1')) - str2float
console.log(parseFloat('1.1')) console.log(Number.parseFloat('1.1')) - 是否为
NaNconsole.log(isNaN(NaN)) console.log(Number.isNaN(NaN)) - 是否有限
console.log(isFinite(1)) console.log(Number.isFinite(1)) - 定时器
setTimeout指定时间后执行一次clearTimeout清除定时器setInterval指定时间间隔反复执行clearInterval清除定时器
其他常用内置对象
- Promise
- async
- await
- 迭代器
- 生成器 yield
- eval
- Proxy
- Reflect
事件循环
在 JavaScript 的事件循环(Event Loop)模型中,最核心的是同步执行栈、宏任务队列、微任务队列
setTimeout(() => console.log(1), 0)
Promise.resolve().then(() => console.log(2))
console.log(3)
// 输出顺序:3 2 1
setTimeout 将一个回调函数放入 宏任务队列(macro task queue),等待主线程执行完后再执行。即:定时器的回调会稍后执行。
.then() 回调属于 微任务(micro task)。 微任务会在当前宏任务(也就是主线程执行完同步代码)之后、下一个宏任务之前执行。
同步代码,立即执行。
最终执行顺序
执行主线程同步代码 输出 3
执行所有微任务(microtasks) 输出 2
执行下一个宏任务(macrotask) 输出 1
async/await
async/await 是 Promise 的语法糖。当你在代码中使用 async 函数时,它的执行分为两部分:
await 之前(包括 await 紧挨着的表达式)的代码:是同步执行的。
await 之后的代码:会被放入微任务队列中,它的行为和 Promise.then() 完全一样。
因此await内的代码也是微任务
更多类型
根据 运行环境(浏览器 vs Node.js) 的不同,还有一些“特殊通道”或“其他队列”:
- 在浏览器环境中:除了宏任务和微任务,还有与 渲染(Rendering) 高度相关的队列:
- requestAnimationFrame (rAF):不属于严格意义上的宏任务或微任务。浏览器在下一次重绘(Paint)之前执行的专门队列。通常用来做流畅的 JavaScript 动画。
- 执行时机:在微任务执行完毕后,浏览器决定要渲染UI之前。
- requestIdleCallback (rIC):顾名思义,在浏览器主线程空闲时才会执行的任务。属于优先级极低的后台任务。如果浏览器很忙,可能一直不执行。
- requestAnimationFrame (rAF):不属于严格意义上的宏任务或微任务。浏览器在下一次重绘(Paint)之前执行的专门队列。通常用来做流畅的 JavaScript 动画。
- 在 Node.js 环境中(非常特殊):Node.js 的事件循环比浏览器更复杂(基于 libuv),除了标准的宏任务(setTimeout 等)和微任务(Promise.then),还有两个专属的机制:
- process.nextTick(VIP 微任务):属于微任务的范畴,但是!拥有最高优先级。
- 在 Node.js 中,每次同步代码执行完准备去清空微任务队列时,会优先清空 process.nextTick 队列,然后再去清空 Promise.then 队列。
- setImmediate(特定的宏任务):属于宏任务,但 Node.js 会安排在事件循环的 Check 阶段执行。一般情况下,如果和 setTimeout(fn, 0) 一起执行,本类型任务的顺序是不确定的(受机器性能影响),但在 I/O 回调中,setImmediate 总是优先于 setTimeout 执行。
- process.nextTick(VIP 微任务):属于微任务的范畴,但是!拥有最高优先级。
总结一张全局执行顺序表(以最常见的场景为例):
- 执行整体代码(全局同步代码)
- 遇到 process.nextTick 加入 Tick 队列(仅 Node.js)
- 遇到 Promise.then/await 后续代码 加入 微任务队列
- 遇到 setTimeout/setInterval 加入 宏任务队列
- 同步代码执行完毕
- 清空 Tick 队列(仅 Node.js)
- 清空 微任务队列(输出示例代码中的
2) - 执行 UI 渲染(仅浏览器,如 requestAnimationFrame)
- 取出一个 宏任务 执行(输出示例代码中的
1) - 重复步骤 6~9...
导入导出
将一些函数、变量等内容暴露出来,供其他 js 文件导入与引用
ES6 Modules 规范
JavaScript 语言官方(ECMAScript 6 标准)在 2015 年正式推出的模块化方案。是静态的语法(在编译阶段就能确定依赖关系)
使用关键字 export export default import
浏览器环境 逐渐支持了这种写法(配合 <script type="module"> )
nodejs环境 要在 package.json 中配置 "type": "module",以便告诉 Node:“请用官方的 ES6 规范来解析我”
export声明暴露: 在声明语句的最开始加上 export 关键字直接进行暴露- 暴露
export var testStr = 'testStr' export function testFunc(){console.log('test')} - 导入
- 全部导入 需要起一个别名作为接收对象,此时导入内容是一个对象
import * as test from ‘./common-js.js’ // 别名 test // 调用 通过别名对象点出来 test.testFunc() - 解构导入 通过
{}将导入的对象解构,可实现 按需导入import {testStr, testFunc} from './myModule.js' import {testStr as str} from './myModule.js' // 解构别名
- 全部导入 需要起一个别名作为接收对象,此时导入内容是一个对象
- 暴露
export统一暴露 (Named Export),将多个内容使用一个对象进行暴露。一个文件可拥有多个独立的 export(包括声明暴露和统一暴露),地位平等。- 暴露
// 正常编写内容 var testStr = 'testStr' function testFunc(){console.log('test')} // 统一暴露 - es6 封包,等效于 {testStr: testStr, testFunc: testFunc} export {testStr, testFunc} - 导入。可使用之前相同的写法
// 与之前类似,用大括号,且按需引入 import { testStr } from './myModule.js'; // 正确 import { testStr as myStr } from './myModule.js'; // 需要将暴露的对象改名时,只能使用 `as`
- 暴露
default暴露 (Default Export),一个文件只能存在一个export default。它是这个模块的“主要输出- 暴露
// 正常编写内容 var testStr = 'testStr' function testFunc(){console.log('test')} // default 暴露对象 export default { testStr: testStr, testFunc: testFunc } - 导入: 导入与之前类似,导入一个对象 或 使用结构进行按需导入
- 全部导入。由于已经指明了默认导出的对象,因此名称可自定义
import A from './myModule.js'; // A 就是那个对象 import B from './myModule.js'; // B 也是那个对象,完全合法 - 解构导入 与之前一样,通过
{}将导入的对象解构,可实现 按需导入
- 全部导入。由于已经指明了默认导出的对象,因此名称可自定义
- 暴露
动态导入: 在代码执行过程中进行导入
var x = true if (x) { import('./myModule.js').then(module => { module.testFunc() }) }
Tips:
按需加载 (Tree-shaking) 的现代前端项目中,不推荐使用 default 暴露,在导入时推荐使用解构导入
因为这样打包工具(如 Webpack/Vite)能清楚地知道只引入了哪些内容,于是会把没用到的内容直接删掉不打包
而如果用的是 default 暴露一个大对象,打包工具往往很难拆解这个对象,只能把整个对象全打包进去,造成代码体积变大
如果一个文件使用了多个 export 声明暴露和 export 统一暴露
可以通过
{}的方式一行导入多个暴露里的内容。如果使用 import * as test from './myModule.js' 的方式导入,会将多个暴露都放到 test 对象里
// ====== 暴露 ====== // 1. 声明暴露了变量和函数 export const name = 'Alice'; export function sayHi() { console.log('Hi!'); } // 2. 正常编写,然后统一暴露 const age = 18; const gender = 'female'; export { age, gender }; // ====== 导入 ====== import { name, sayHi, age } from './myModule.js'; console.log(name); // 'Alice' console.log(age); // 18 sayHi(); // 输出 'Hi!'如果一个文件使用了多个 export 声明暴露和 export 统一暴露 和一个 default 暴露。使用对象进入导入,对象的
default属性指向default暴露的内容// ====== 暴露 ====== export const name = 'Alice'; // 声明暴露 export { age: 18 }; // 统一暴露 export default function() { // default 暴露 console.log('我是默认导出的函数'); } // ====== 导入 ====== import * as test from './myModule.js'; console.log(test.name); // 'Alice' // 怎么调用 default 暴露的内容? test.default(); // 输出 '我是默认导出的函数'
CommonJS 规范
Node.js 诞生之初为了解决服务器端模块化问题,自己采用的一套社区规范
关键字:module.exports exports require()
导入的是动态的(在代码运行到 require 那一行时才会去同步加载文件)
- 暴露 使用
module.exports进行统一暴露- 暴露单个函数
function decrypt(code) { return code } module.exports = decrypt // 暴露了一个函数对象 - 暴露多个内容: 与 ES6 规范的 统一暴露与 default 暴露类似,将多个内容封包成一个对象
function a() {} function b() {} module.exports = {a, b}
- 暴露单个函数
- 导入: 使用
require接收暴露的对象// 当暴露的是函数对象时 const func = require('./decrypt.js'); // 接收到的就是一个函数对象,可自定义名称 func() // 此处的 func 函数就是 decrypt 函数 const utils = require('./utils'); utils.a(); utils.b();
Tips: module.exports 到底是什么?
- Node 在每个文件背后偷偷包了一层
(function (exports, require, module, __filename, __dirname) { // 文件内的代码在这里 }); - 这是 Node.js 在运行时为每一个 CommonJS 文件自动加上的“函数外壳”。其中:
module.exports:最终导出的东西exports:只是module.exports的一个引用
- webpack 在浏览器环境中模拟了 node 中的 common js 以实现隔离
// 分发器中代码 return __webpack_modules__[moduleId](module, module.exports, __webpack_require__);- 浏览器环境 原生没有 CommonJS, 但 Node 模块设计太成功了
- webpack 就在 浏览器环境 里 复刻了一套 Node 模块运行时
- 用函数包裹模块实现浏览器模块化