本文记录现代 Javascript 的一些学习笔记,从头到尾的全面学习下 javascript。 从文档上看,这个教程的质量和细节部分还是挺多的。记录的内容大多是我没有接触过,或者比较模糊的部分。
空值合并运算符
??
是用于在值为null/undefined
这两个情况下返回第一个已经定义的值。 这个比||
行为不同的是:它对0/""
等这些情况会认为是已定义的。??
的优先级非常低,需要注意使用括号。
> let firstName = null
undefined
> let secondname = ''
undefined
> let c = secondname ?? 'me?'
undefined
> c
''
> let d = firstName ?? 'me!'
undefined
> d
'me!'
>
同时可以通过多个串起来使用:
> let d = firstName ?? 'me!'
undefined
> d
'me!'
> let e=null
undefined
> let g = e ?? firstName ?? secondname ?? d
undefined
> g
''
>
这边就会返回secondname
这个非空的值。
函数
- 不要在
return
与返回值之间添加新行,这会导致 return 变成单行指令,返回空置;
// bad case
return // will be treat as return;
(a + b)
// optimized
return (
a + b
)
- 函数表达式后尽量增加一个
;
let add = function (a, b) {
return a + b;
};
函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。
代码质量
debugger;
单独的语句会让调试器在此暂停住,并且不会执行下一条语句。- 当处于调试模式时,在代码中的某一行上右键,在显示的关联菜单(context menu)中点击一个非常有用的名为 “Continue to here” 的选项。
文档注释
语言规范
Object
- 多单词的 key 需要使用引号包裹
let obj = {
a: 1,
b: 2,
"hello world": 3,
};
console.log(obj['hello world']) // 3
key='hello world'
console.log(obj[key]) // 3
console.log(obj.key) // undefined
- 当数字 0 被用作对象的属性的键时,会被转换为字符串 “0”
- 整数属性会被排序,其他的属性会按照创建的顺序排序
let codes = {
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
for(let code in codes) {
alert(code); // 1, 41, 44, 49
}
使用 const 声明的对象也是可以被修改的
Object.assign 来做所谓的“浅拷贝”, 因为嵌套的对象同样存在引用
> const abc = {'a': 1, 'c': {'d': 2}}
> abc
{ a: 1, c: { d: 2 } }
> abcd = Object.assign({}, abc)
{ a: 1, c: { d: 2 } }
> abcd.c.d=4
4
> abc
{ a: 1, c: { d: 4 } }
>
构造函数
常规来说,它们也是普通的函数,不过有如下的规定:
- 它们的命名以大写字母开头。
- 它们只能由 “new” 操作符来执行。
JS 中形如Date
或者Set
等等的构造函数,都需要使用new
操作符来创建。 通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。但是,如果这有一个 return 语句,那么规则就简单了:
+ 如果 return 返回的是一个对象,则返回这个对象,而不是 this。
+ 如果 return 返回的是一个原始类型,则忽略。
一个例子:
function Person(name) {
this.name = name;
return {"newname" : this.name }
}
p = new Person('John')
console.log(p) // { newname: 'John' }
// normal variable
function Person2(name) {
this.name = name;
let age = 18;
return age;
}
p2 = new Person2('John')
console.log(p2) // { name: 'John' }
可选链
?.
这个一般用在判断一个 object 的属性是否存在,如果存在,则返回这个属性的值,否则返回 undefined。 所谓的存在一般指的是这个属性是非 null/undefined
的。
ret = a?.b?.c?.d
这可以省去大部分的重复判断代码。除此之外还有?.()
和?.[]
这两个变体。 这些是可以用于删除和读取的,但是不能用于写入。
let userAdmin = {
admin() {
alert("I am admin");
}
};
let userGuest = {};
userAdmin.admin?.(); // I am admin
userGuest.admin?.(); // 啥都没有(没有这样的方法)
// 可选 key
let user = null
user?.['helloworld'] // undefined
delete user?.key // 此处不存在key,所以不会删除,但不会报错
注意以上再不存在时均返回undefined
。
Symbol
Symbol
表示唯一的标识符。可以用于在 object 中,作为 object 的属性名,但是它不会被转换成字符串,是属于独立的类型。 如果想要获取他的定义或者是值,使用如下:
let id = Symbol("id")
console.log(id.toString()) // Symbol(id)
console.log(id.description) // id
symbol 的隐藏属性可以让我们安全的在第三方的结构里面增加新的key。 比如:
let id = Symbol("id")
let user = {
name: "John",
}
user[id] = 123
console.log(usr[id]) // 123
- 在
for...in
循环中,Symbol 属性会被忽略。 - 在
Object.keys()
中,Symbol 属性会被忽略。 - 在
Object.assing()
中,Symbol 属性会被复制。
全局 symbol
- 使用
Symbol.for("xxx")
来获取全局 symbol。 如果不存在就创建一个新的。存在的就直接返回。 - 使用
Symbol.keyFor("xxx")
来获取 symbol 的名字
从技术上说,Symbol 不是 100% 隐藏的。有一个内建方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 Symbol。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 Symbol。所以它们并不是真正的隐藏。但是大多数库、内建方法和语法结构都没有使用这些方法。
primitive
可以使用Symbol.primitive
的内建 symbol 用来转换方法命名,这个类似于将 object 变成可以处理不同场景下的情况。
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"}
console.log(+user); // hint: number -> 1000
console.log(user + 500); // hint: default -> 1500
默认情况下,普通对象具有 toString 和 valueOf 方法:
- toString 方法返回一个字符串 “[object Object]”。
- valueOf 方法返回对象自身。
let user = {name: "john"}
console.log(user.valueOf()) // {name: "john"}
此时我们可以重写toString
和valueOf
方法
let user = {
name: "john",
value: 500,
toString() {
return `{name: "${this.name}"}`
},
valueOf() {
return this.value
}
}
alert(user) // {name: "John"}
console.log(user + 500) // 1000
数字类型
toString(base)
返回当前数字以 base 形式的字符串。
> let number = 100
100
> number.toString(2)
'1100100'
> number.toString(16)
'64'
>
同样的当我们将字符串的数字变成数字时,parseInt
和parseFloat
可以帮助我们。不过这parseInt
拥有第二个参数,用来制定进制。
> let anumber = '100'
> parseInt(anumber) // 100
> parseInt(anumber, 2) // 4, 将 100 看成是二进制的
String
字符串的indexOf
的返回值可以使用一个按位与的小技巧来判断。如下:
let str = "hello world";
if(~str.indexOf("o")) {
console.log("find it")
}
~
就是按位取反, 所以~n
等价于-(n+1)
。 也就是说~-1
= 0。 0 在 js 中是表示 falsy 值的。不过这个技巧不太建议使用,因为有一定的局限性,且我们可以使用includes
来替代。
一些常用的方法的总结:
| 方法 | 选择方式 | 负数参数 |
| ---- | ---- | ---- |
| slice(start, end) | 从 start 到 end(不含 end)| 允许 |
| substring(start, end) | start 与 end 之间(包括 start,但不包括 end)| 负值代表 0 |
| substr(start, length) | 从 start 开始获取长为 length 的字符串 | 允许 start 为负数 |
array
循环遍历数组可以有如下两种方式:
for (let i=0; i<arr.length; i++)
— 运行得最快,可兼容旧版本浏览器。for (let item of arr)
— 现代语法,只能访问 items。for (let i in arr)
— 永远不要用这个。
数组默认有一个toString
的操作。所以我们可以如下操作:
> let a = [1,2,3,4]
> String(a)
'1,2,3,4'
splice(start[, deleteCount, elem1, …, elemN])
- 第一个参数表示
start
的位置;可以是负数,也可以是 0 等等; - 第二个参数表示要删除多少个元素,可以是 0;
- 往后的参数表示需要插入的元素,从 start 的位置之后开始。
concat
连接多个数组,如果一个 object 是可以被看作数组时,也是可以使用:
> let arr = [1,2]
> let obj = {
... "index": 1,
... "array": 2,
... [Symbol.isConcatSpreadable]: true,
... length: 2 }
> arr.concat(obj)
[ 1, 2, <2 empty items> ]
> let arrayLike = {
... 0: "something",
... 1: "else",
... [Symbol.isConcatSpreadable]: true,
... length: 2
... };
> arr.concat(arrayLike)
[ 1, 2, 'something', 'else' ]
搜索
arr.indexOf(item, from)
从索引 from 开始搜索 item,如果找到则返回索引,否则返回 -1。arr.lastIndexOf(item, from)
—— 和上面相同,只是从右向左搜索。arr.includes(item, from)
—— 从索引 from 开始搜索 item,如果找到则返回 true(译注:如果没找到,则返回 false)。
find/findIndex
let result = arr.find(function(item, index, array) {
// 如果返回 true,则返回 item 并停止迭代
// 对于假值(falsy)的情况,则返回 undefined
});
数组可以拍平,一般是flat
。
var arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]
var arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]
//使用 Infinity,可展开任意深度的嵌套数组
var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
具体我们可以参考:
此外还有一个小技巧,Array
和Array.of
:
> Array(7)
[ <7 empty items> ]
> Array.of(7)
[ 7 ]
> Array.of(null)
[ null ]
> Array.of(undefined)
[ undefined ]
迭代
通过 [Symbol.iterator]` 实现 range
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
console.log(num); // 1, 然后是 2, 3, 4, 5
}
Iterable
如上所述,是实现了 Symbol.iterator 方法的对象。Array-like
是有索引和 length 属性的对象,所以它们看起来很像数组。
可以使用Array.from
对一个 string 类型的变量转换成 array。 比如:
> let chars = Array.from("hello world")
> chars
[
'h', 'e', 'l', 'l',
'o', ' ', 'w', 'o',
'r', 'l', 'd'
]
Map
new Map()
—— 创建 map。map.set(key, value)
—— 根据键存储值。map.get(key)
—— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。map.has(key)
—— 如果 key 存在则返回 true,否则返回 false。map.delete(key)
—— 删除指定键的值。map.clear()
—— 清空 map。map.size
—— 返回当前元素个数。
map[key] 不是使用 Map 的正确方式。 应当使用
set
和get
map 迭代
如果要在 map 里使用循环,可以使用以下三个方法:
map.keys()
—— 遍历并返回所有的键(returns an iterable for keys),map.values()
—— 遍历并返回所有的值(returns an iterable for values),map.entries()
—— 遍历并返回所有的实体(returns an iterable for entries)[key, value],for…of 在默认情况下使用的就是这个。
此外还有两个比较方便的方法,用于 map 和 object 之间的转换
- Object.fromEntries 将 map 转换成 Object
- Object.entries 生成
[[key, value], [key, value]]
可以用于创建 map
Set
new Set(iterable)
—— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。set.add(value)
—— 添加一个值,返回 set 本身set.delete(value)
—— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false。set.has(value)
—— 如果 value 在 set 中,返回 true,否则返回 false。set.clear()
—— 清空 set。set.size
—— 返回元素个数。
WeakMap / WeakSet
WeakMap
的键只能是object
。 当在 object 被清理时,对应的 weakmap 中的内容也会被清理。 weakmap 适合在:
- 需要有缓存,当一个函数的结果需要被记住(“缓存”),这样在后续的对同一个对象的调用时,就可以重用这个被缓存的结果。当对象被垃圾回收时,对应的缓存的结果也会被自动地从内存中清除。
- 额外的数据,将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除
注意, weakmap 只支持如下的几个方法:
- weakMap.get(key)
- weakMap.set(key, value)
- weakMap.delete(key)
- weakMap.has(key)
这两个数据结构均不支持迭代。所以需要在合适的时机使用它们。
Object.keys, values, entries
Object.keys/values/entries 会忽略 symbol 属性
Object.fromEntries(
// 转换为数组,之后使用 map 方法,然后通过 fromEntries 再转回到对象
Object.entries(prices).map(([key, value]) => [key, value * 2])
);
数组解构
let arr = ["Firstname", "Lastname"]
let [firstname, lastname] = arr
如果不想要某个元素可以使用,
来忽略对应位置上的内容
let [a, ,c] = ['a', 'b', 'c']
console.log(c)
实际上等号右侧可以是任何可迭代的内容,也可以是任何可以赋值的内容。
任何剩余的内容,可以使用...
来接收。比如:
let [a, b, ...c] = [1,2,3,4,5,6,7]
console.log(c) = [3,4,5,6,7]
数组解构赋值的默认值是undefined
结构解构
我们一般使用结构的字段名来直接获取对应的字段的 value。 也可以将对应的字段的值赋值给自定义的变量。
let options = {
title: 'menu',
width: 100,
height: 200
}
let {title, width: w, height: h} = options
对于不一定存在的字段,我们可以使用=
设置默认值。这样避免因为字段不存在而报错。
let options = {
title: 'menu'
}
let {width = 100, height = 200, title} = options
//同样可以做到重命名
let {width: w = 100, height: h = 200, title} = options
对应剩余的内容一样可以使用...
。
可以使用前套的解构,一个基本的例子:
let options = {
size: {
width: 100,
height: 200
},
items: ['hello', 'world'],
extra: true
}
let { size: {width, height}, items:[item1, item2], noexist=true} = options
智能函数参数
一个函数需要接收很多的参数时候,我们一般通过传递一个结构到函数内,此时可以使用结构或者是嵌套解构的方式进行:
function showMenu({ title = "Menu", width: w = 100, height = 200 } = {}) {
alert( `${title} ${w} ${height}` );
}
showMenu(); // Menu 100 200
时间
let date = new Date()
// 时间戳,带毫秒
date.getTime() // 或者使用 +date 效果一样
// 增加 24 小时,单位毫秒
let onedayLater = new Date(24 * 3600 * 1000)
// 解析时间
let date = new Date('2022-01-02')
//可以完全指定时间
new Date(2011, 0, 1, 0, 0, 0, 0) //1 Jan 2011, 00:00:00
一些比较方便的访问日期的组件:
- getFullYear()
- getMonth() // 0-11
- getDate() // 只返回 1-31 之间的数字
- getHours()
- getMinutes()
- getSeconds()
- getMilliseconds()
- getDay() 返回一周中的第几天 0-6, 0表示周日
同时,我们可以使用一些设置时间的函数对时间进行设置。这些主要是用来配置一些指定的时间
- setFullYear(year, [month], [date])
- setMonth(month, [date])
- setDate(date)
- setHours(hour, [min], [sec], [ms])
- setMinutes(min, [sec], [ms])
- setSeconds(sec, [ms])
- setMilliseconds(ms)
- setTime(milliseconds)(使用自 1970-01-01 00:00:00 UTC+0 以来的毫秒数来设置整个日期)
两个时间可以直接相减,结果是 ms 的时间差。 一个更快用于计算时间差的方式是:
let start = Date.now()
....
let end = Date.now()
let diff = end-start
可以使用Date.parse(str)
来解析日期, 字符串的格式应该为:YYYY-MM-DDTHH:mm:ss.sssZ
,其中:
- YYYY-MM-DD —— 日期:年-月-日。
- 字符 “T” 是一个分隔符。
- HH:mm:ss.sss —— 时间:小时,分钟,秒,毫秒。
- 可选字符 ‘Z’ 为 ±hh:mm 格式的时区。单个字符 Z 代表 UTC+0 时区。
JSON
一般我们使用JSON.parse
和JSON.stringify
这两个方法。 其中JSON.stringify
有两个不太常用参数。JSON.stringify(value, replacer, spaces)
- replacer
let object = {
title: 'Hello',
number: 10,
arr: [1,2,3]
}
// 使用数组
JSON.stringify(object, ['arr']) // '{"arr":[1,2,3]}'
// 使用函数
JSON.stringify(object, function replacer(key, value) {
console.log(`${key}: ${value}`);
return (key == 'arr') ? undefined : value;
}) // '{"title":"Hello","number":10}'
- space 主要是用来进行美化输出的。
自定义 toJSON
像 toString 进行字符串转换,对象也可以提供 toJSON 方法来进行 JSON 转换。如果可用,JSON.stringify 会自动调用它。
let room = {
number: 23,
toJSON() {
return this.number;
}
};
let meetup = {
title: "Conference",
room
};
JSON.stringify(room) // 23
JSON.stringify(meetup)
/*
{
"title":"Conference",
"room": 23
}
*/
JSON.parse
同样的,JSON.parse 是可以接收一个 receiver 参数的,用于解析一些特殊字段,或者是需要自定义处理的内容。比如:
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;
});
注意, replacer 的第一次会返回一个 key 是空,value 是自身的结果,这个在操作的时候需要考虑到。
函数
我们可以通过functionname.name
来获取一个函数的名称。比如:
function sayYeah() {
console.log('yeah')
}
console.log(sayYeah.name) // sayYeah
可以通过length
来获取到函数的参数,不过 rest 的参数不会被统计。
function f1(a,b, ...c) {}
console.log(f1.length) // 2
可以给函数自定义一些变量,这样可以做到在函数外部也是可以访问到对应的变量的,这个和闭包是有所不同的。
function makeCounter() {
function counter() {
return counter.count++
}
counter.count= 0
return counter
}
let counter = makeCounter()
console.log(counter()) // 0
console.log(counter()) // 1
console.log(counter.count) // 2
console.log(counter.count) // 2
函数的 NFE(Named Function Expression), 可以在函数被赋值了另外的一变量的时候仍然可以调用。
let sayhi = function func(name) {
if(name) {
console.log(name)
} else {
func("default")
}
}
let sayhi2 = sayhi
let welcome = sayhi
welcome()
sayhi = null
sayhi2()
setTimeout / setInterval
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
- setTimeout 允许我们将函数推迟到一段时间间隔之后再执行。
- setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。
let tid = setTimeout(() => { console.log("say hello")}, 5000);
clearTimeout(tid) // 取消推迟执行
嵌套的 setTimeout 能够精确地设置两次执行之间的延时,而 setInterval 却不能。
这边需要注意的是setInterval
是一个固定间隔,如果执行的函数耗时超过这个间隔,setInterval 还是会在指定时间内调用函数。 而用两次嵌套的 setTimeout 可以比较均匀一些。
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
注意,例如,浏览器内的计时器可能由于许多原因而变慢:
- CPU 过载。
- 浏览器页签处于后台模式。
- 笔记本电脑用的是电池供电(译注:使用电池供电会以降低性能为代价提升续航)。
所有这些因素,可能会将定时器的最小计时器分辨率(最小延迟)增加到 300ms 甚至 1000ms,具体以浏览器及其设置为准。
任何 setTimeout 都只会在当前代码执行完毕之后才会执行。
对象属性配置
可以通过Object.getOwnPropertyDescriptor(obj, propertyName)
获取一个对象的指定字段的属性描述信息。 同时可以使用Object.defineProperty(obj, propertyName, descriptor)
去设置属性。
这边一般会涉及到三个标志:
- writable
- enumerable
- configurable
使用defineProperty
可以设置这些标志。 当某个属性的enumerable
被设置成 false 时,则该属性将不会出现在for ... in ...
中。 当我们将configurable
设置成 false 时,则我们无法删除该属性,且该过程不可逆。
此外我们可以使用defineProerties
来设置多个属性的标志。同理可以使用getOwnPropertyDescriptors
获取多个属性的。
可以使用如下方式进行 object 的克隆
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
对象还包含两个访问器属性,getter
和setter
。即如下的代码:
let obj = {
get propName() {
}
set propName() {
}
}
一个 Object 的属性不能既是访问器,有是数据属性,比如下面的方式就是错误的。
//error code
Object.defineProperty({}, 'prop', {
get() {
return 1
}
value: 2
})
同样访问属性的描述符也可以使用defineProperty
来设置。除了value
和writable
这两个不能设置外,我们可以设置get
,set
,enumerable
和configurable
这四个。
原型
可以使用__proto__
设置,也可以通过getPrototypeOf
和setPrototypeOf
来代替__proto__
的设置方式。
for ... in
循环也会迭代继承的属性。但是Object.keys
只会返回自己的 key。
可以使用obj.hasOwnProperty(key)
的方式来判断 key 是否是 obj 自身的,而不是痛殴继承获得的。
F.prototype
当我们在使用new
关键字创建一个新的对象时,同样可以通过继承的方式来获取属性。
function Rabbit(name) {
this.name = name
}
let animal = {
eats: true
}
Rabbit.prototype = animal
let rabbit = new Rabbit("White Rabbit")
console.log(rabbit.eats) // true
每个函数都有构造器属性,默认的 prototype 只有一个属性constructor
的对象,这个属性指向函数本身。
prototyepe 属性在常规的对象上没有什么特殊的含义,只是一个普通的字段,但是在函数里,就有了特殊意义了
原生的原型
- 所有的内建对象都遵循相同的模式(pattern):
- 方法都存储在 prototype 中(Array.prototype、Object.prototype、Date.prototype 等)。
- 对象本身只存储数据(数组元素、对象属性、日期)。
- 原始数据类型也将方法存储在包装器对象的 prototype 中:Number.prototype、String.prototype 和 Boolean.prototype。只有 undefined 和 null 没有包装器对象。
- 内建原型可以被修改或被用新的方法填充。但是不建议更改它们。唯一允许的情况可能是,当我们添加一个还没有被 JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准时,才可能允许这样做。
如果要将一个用户生成的键放入一个对象,那么内建的protogetter/setter 是不安全的。因为用户可能会输入 “proto” 作为键,这会导致一个 error,虽然我们希望这个问题不会造成什么大影响,但通常会造成不可预料的后果。
因此,我们可以使用 Object.create(null) 创建一个没有proto的 “very plain” 对象,或者对此类场景坚持使用 Map 对象就可以了。