一、变量声明(let、const)
1.1 let/const的基本使用
-
let关键字:
- 从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量
-
const关键字:
- 表示保存的数据一旦被赋值,就不能被修改
- 如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容
-
注意:let、const不允许重复声明变量
// 1.let
let message = "你好, 世界"
message = 123
console.log(message) // 123
// 不允许重复声明
// let message // Uncaught SyntaxError: Identifier 'message' has already been declared
// 2.const
// const message2 = "nihao, shijie"
// message2 = "nihao, China" // Uncaught TypeError: Assignment to constant variable.
// 赋值引用类型
const info = {
name: "kobe",
age: 24
}
// info = {} // Uncaught TypeError: Assignment to constant variable.
info.name = "james"
console.log(info)
1.2 let/const有没有作用域提升
-
标识符在词法环境被创建时, 会被提前创建出来, 但是不能访问
// 1.var声明的变量会进行作用域的提升 // console.log(message) // var message = "Hello World" // 2.let/const声明的变量: 没有作用域提升 // console.log(address) console.log(info) // Uncaught ReferenceError: Cannot access 'info' before initialization let address = "北京市" const info = {}
- 有没有作用域提升呢?
- 作用于提升的概念:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升
- 所以let、const没有进行作用域提升,但是会在解析阶段被创建出来
1.3 暂时性死区TDZ
-
从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(TDZ,temporal dead zone)
// 1.暂时性死区 // function foo() { // console.log(bar, baz) // Uncaught ReferenceError: Cannot access 'bar' before initialization // let bar = "bar" // let baz = "baz" // } // foo() // 2.暂时性死区和定义的位置没有关系, 和代码执行的顺序有关系 // function foo() { // console.log(message) // } // let message = "Hello World" // foo() // Hello World // console.log(message) // 3.暂时性死区形成之后, 在该区域内这个标识符不能访问 let message = "Hello World" function foo() { console.log(message) // Uncaught ReferenceError: Cannot access 'message' before initialization const message = "哈哈哈哈" } foo()
1.4 不会添加window
-
全局通过var来声明一个变量,事实上会在window上添加一个属性,但是let、const是不会给window上添加任何属性
// 1.var定义的变量是会默认添加到window上的 // var message = "Hello World" // var address = "北京市" // console.log(window.message) // console.log(window.address) // 2.let/const定义的变量不会添加到window上的 // let message = "Hello World" // let address = "北京市" // console.log(window.message) // undefined // console.log(window.address) // undefined
1.5 块级作用域和应用
- JavaScript中的作用域:全局作用域、函数作用域、块级作用域
- let/const/class/function会形成块级作用域
- 函数拥有块级作用域,但是外面依然是可以访问的:这是因为引擎会对函数的声明进行特殊的处理,允许像var那样在外界直接访问
// foo() // foo is not a function { var message = "Hello World" let age = 18 const height = 1.88 class Person {} function foo() { console.log("foo function") } } // console.log(age) // Uncaught ReferenceError: age is not defined // console.log(height) // Uncaught ReferenceError: height is not defined // const p = new Person() // Uncaught ReferenceError: Person is not defined foo() // foo function
- let的块级作用域的应用
-
btnEl的for循环
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button>按钮0</button> <button>按钮1</button> <button>按钮2</button> <button>按钮3</button> <script> // 监听按钮的点击 const btnEls = document.querySelectorAll("button") // [btn1, btn2, btn3, btn4] // for (var i = 0; i < btnEls.length; i++) { // var btnEl = btnEls[i]; // // btnEl.index = i // (function(m) { // btnEl.onclick = function() { // debugger // console.log(`点击了${m}按钮`) // } // })(i) // } for (let i = 0; i < btnEls.length; i++) { const btnEl = btnEls[i]; btnEl.onclick = function() { console.log(`点击了${i}按钮`) } } </script> </body> </html>
-
1.6 var、let、const的选择
- 对于var的使用:
- var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题,其实是JavaScript在设计之初的一种语言缺陷
- 在实际工作中,可以使用最新的规范来编写,也就是不再使用var来定义变量了
- 对于let、const:
- 优先推荐使用const,这样可以保证数据的安全性不会被随意的篡改
- 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let
- 这种在很多其他语言里面也都是一种约定俗成的规范,尽量也遵守这种规范
二、函数扩展
2.1 默认参数
- 写法:在参数后面写默认值
function foo(arg1 = "我是默认值", arg2 = "我也是默认值") {
// 1.两种写法不严谨
// 默认值写法一:
// arg1 = arg1 ? arg1: "我是默认值"
// 默认值写法二:
// arg1 = arg1 || "我是默认值"
// 2.严谨的写法
// 三元运算符
// arg1 = (arg1 === undefined || arg1 === null) ? "我是默认值": arg1
// ES6之后新增语法: ??
// arg1 = arg1 ?? "我是默认值"
// 3.简便的写法: 默认参数
console.log(arg1)
}
- 注意事项:
- 默认参数是不会对
null进行处理的 - 有默认参数的形参尽量写到后面
- 有默认参数的形参, 是不会计算在length之内(并且后面所有的参数都不会计算在length之内)
- 剩余参数也是放到后面(默认参数放到剩余参数的前面)
- 默认参数是不会对
function foo(age, name = "zhangsan", ...args) {
console.log(name, age, args)
}
foo(18, "abc", "cba", "nba")
console.log(foo.length) // 1
- 默认参数的解构
function foo({ name = "lilei", age = 18 } = {}) {
console.log(name, age)
}
foo()
2.2 剩余参数
-
将不定数量的参数放入到一个数组中:如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
-
剩余参数和arguments有什么区别呢?
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参
- arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作
- arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的
function foo(num1, num2, ...otherNums) {
// otherNums数组
console.log(otherNums) // [111, 222, 333]
}
foo(20, 30, 111, 222, 333)
- 注意:剩余参数必须放到最后一个位置,否则会报错。
2.3 扩展运算符
- 展开语法(Spread syntax):
- 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开
- 可以在构造字面量对象时, 将对象表达式按key-value的方式展开
// 1.基本演练
// ES6
const names = ["abc", "cba", "nba", "mba"]
const str = "Hello"
// const newNames = [...names, "aaa", "bbb"]
// console.log(newNames)
function foo(name1, name2, ...args) {
console.log(name1, name2, args)
}
foo(...names) // abc cba ['nba', 'mba']
foo(...str) // H e ['l', 'l', 'o']
// ES9(ES2018)
const obj = {
name: "lilei",
age: 18
}
// 不可以这样来使用
// foo(...obj) // 在函数的调用时, 用展开运算符, 将对应的展开数据, 进行迭代
// 可迭代对象: 数组/string/arguments
const info = {
...obj,
height: 1.68,
address: "北京市"
}
console.log(info) // {name: 'lilei', age: 18, height: 1.68, address: '广州市'}
- 注意:展开运算符其实是一种浅拷贝
const obj = {
name: "lilei",
age: 18,
height: 1.68,
friend: {
name: "hanmeimei"
}
}
// 1.引用赋值
// const info1 = obj
// 2.浅拷贝
const info2 = {
...obj
}
info2.name = "kobe"
console.log(obj.name) // lilei
console.log(info2.name) // kobe
// info2.friend.name = "james"
// console.log(obj.friend.name) // james
// 3.深拷贝
// 方式一: 第三方库
// 方式二: 自己实现
// function deepCopy(obj) {}
// 方式三: 利用现有的js机制, 实现深拷贝JSON
const info3 = JSON.parse(JSON.stringify(obj))
console.log(info3.friend.name) // hanmeimei
info3.friend.name = "james"
console.log("info3.friend.name:", info3.friend.name) // james
console.log(obj.friend.name) // hanmeimei
2.4 箭头函数
- 传送门:JavaScript-箭头函数
- 补充:
- 箭头函数是没有显式原型prototype的,所以不能作为构造函数,使用new来创建对象
- 箭头函数也不绑定this、arguments、super参数
// 在ES6之后, 定义一个类要使用class定义
var bar = () => {}
console.log(bar.__proto__ === Function.prototype) // true
// 没有显式原型
console.log(bar.prototype) // undefined
// var b = new bar() // 报错
三、对象扩展
字面量的增强
const name = "kobe"
const age = 24
const key = "address" + " city"
const obj = {
// 1.属性的增强
// name: name,
name,
age,
// 2.方法的增强
running: function() {
console.log(this)
},
swimming() {
console.log(this)
},
eating: () => {
console.log(this)
},
// 3.计算属性名
[key]: "北京"
}
obj.running() // obj
obj.swimming() // obj
obj.eating() // window
四、字符串扩展
4.1 模板字符串
- ES6允许使用字符串模板来嵌入JS的变量或者表达式来进行拼接:
- 使用 `` 符号来编写字符串,称之为模板字符串,在模板字符串中,我们可以通过
${expression}来嵌入动态的内容
- 使用 `` 符号来编写字符串,称之为模板字符串,在模板字符串中,我们可以通过
const name = "lilei"
const age = 18
// ES6之前
// const info = "my name is" + name + ", age is " + age
// ES6之后
const info = `my name is ${name}, age is ${age}`
console.log(info)
4.2 标签模板字符串
- JavaScript的函数如果使用标签模板字符串,并且在调用的时候插入其他的变量:
- 模板字符串被拆分了
- 第一个元素是数组,是被模块字符串拆分的字符串组合
- 后面的元素是一个个模块字符串传入的内容
// 标签模板字符串的用法
const name = "lilei"
const age = 18
function foo(...args) {
console.log("参数:", args)
}
// foo("kobe", 24, 1.88)
foo`my name is ${name}, age is ${age}, height is ${1.88}` // 参数: [Array(4), 'lilei', 18, 1.88]
五、解构赋值
ES6中新增了一个从数组或对象中方便获取数据的方法,称之为解构Destructuring
- 具体使用见代码:
const names = ["abc", "cba", undefined, "nba", "mba"]
// 1.数组的解构
// 1.1. 基本使用
// const [name1, name2, name3] = names
// console.log(name1, name2, name3)
// 1.2. 顺序问题: 严格的顺序
// const [name1, , name3] = names
// console.log(name1, name3)
// 1.3. 解构出数组
// const [name1, name2, ...newNames] = names
// console.log(name1, name2, newNames)
// 1.4. 解构的默认值
const [name1, name2, name3 = "default"] = names
console.log(name1, name2, name3)
// 2.对象的解构
const obj = { name: "kobe", age: 24, height: 1.88 }
// 2.1. 基本使用
// const { name, age, height } = obj
// console.log(name, age, height)
// 2.2. 顺序问题: 对象的解构是没有顺序, 根据key解构
// const { height, name, age } = obj
// console.log(name, age, height)
// 2.3. 对变量进行重命名
// const { height: wHeight, name: wName, age: wAge } = obj
// console.log(wName, wAge, wHeight)
// 2.4. 默认值
const {
height: wHeight,
name: wName,
age: wAge,
address: wAddress = "中国"
} = obj
console.log(wName, wAge, wHeight, wAddress)
// 2.5. 对象的剩余内容
const {
name,
age,
...newObj
} = obj
console.log(newObj) // {height: 1.88}
// 应用: 在函数中(其他类似的地方)
function getPosition({ x, y }) {
console.log(x, y)
}
getPosition({ x: 10, y: 20 })
getPosition({ x: 25, y: 35 })
function foo(num) {}
foo(123)
六、数值扩展
6.1 规范二进制和八进制的写法
console.log(100)
// 二进制
console.log(0b100) // 4
// 八进制
console.log(0o100) // 64
// 十六进制
console.log(0x100) // 256
6.2 ES12新增数字连接符
- 在ES12(2021)新增特性:数字过长时,可以使用
_作为连接符
const money = 100_00_00_0000_00_00
七、Symbol
Symbol是ES6中新增的一个基本数据类型,翻译为符号
7.1 基本用法
- 在ES6之前,对象的属性名都是字符串形式,很容易造成属性名的冲突
- 比如原来有一个对象,要在其中添加一个新的属性和值,但是不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性
- Symbol用来生成一个独一无二的值
- Symbol值是通过Symbol函数来生成的,生成后可以作为属性名,即在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
const s1 = Symbol()
const obj = {
[s1]: "aaa"
}
console.log(obj)
const s2 = Symbol()
obj[s2] = "bbb"
console.log(obj)
function foo(obj) {
const sKey = Symbol()
obj[sKey] = function() {}
delete obj[sKey]
}
foo(obj)
7.2 Symbol作为属性名
const s1 = Symbol() // aaa
const s2 = Symbol() // bbb
// 1.加入对象中
const obj = {
name: "kobe",
age: 24,
[s1]: "aaa",
[s2]: "bbb"
}
// const obj1 = {}
// obj1[s1] = "aaa"
// obj2[s2] = "bbb"
// const obj2 = {}
// Object.defineProperty(obj2, s1, {
// value: "aaa"
// })
// 2.获取symbol对应的key
console.log(Object.keys(obj))
console.log(Object.getOwnPropertySymbols(obj))
const symbolKeys = Object.getOwnPropertySymbols(obj)
for (const key of symbolKeys) {
console.log(obj[key])
}
7.3 相同值的Symbol
- 如何创建一个相同的Symbol
- 使用Symbol.for方法来做到这一点,并且可以通过
Symbol.keyFor方法来获取对应的key
- 使用Symbol.for方法来做到这一点,并且可以通过
// 1.Symbol函数直接生成的值, 都是独一无二
const s1 = Symbol("ccc")
console.log(s1.description) // ccc
const s2 = Symbol(s1.description)
console.log(s1 === s2) // false
// 2. 如果相同的key, 通过Symbol.for可以生成相同的Symbol值
const s3 = Symbol.for("ddd")
const s4 = Symbol.for("ddd")
console.log(s3 === s4) // true
// 获取传入的key
console.log(Symbol.keyFor(s4)) // ddd
八、Set集合
8.1 Set基本使用
Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复
- 创建Set需要通过Set构造函数(暂时没有字面量创建的方式)
// 1.创建Set
const set = new Set()
console.log(set) // Set(0) {size: 0}
// 2.添加元素
set.add(10)
set.add(22)
set.add(35)
set.add(22)
console.log(set) // Set(3) {10, 22, 35}
const info = {}
const obj = {name: "obj"}
set.add(info)
set.add(obj)
set.add(obj)
console.log(set) // Set(5) {10, 22, 35, {…}, {…}}
- 应用
- 数组去重
const names = ["abc", "cba", "nba", "cba", "nba"]
// const newNames = []
// for (const item of names) {
// if (!newNames.includes(item)) {
// newNames.push(item)
// }
// }
// console.log(newNames)
const newNamesSet = new Set(names)
const newNames = Array.from(newNamesSet)
console.log(newNames)
8.2 Set常见方法和属性
- Set常见的属性:
- size:返回Set中元素的个数;
- Set常用的方法:
- add(value):添加某个元素,返回Set对象本身
- delete(value):从set中删除和这个值相等的元素,返回boolean类型
- has(value):判断set中是否存在某个元素,返回boolean类型
- clear():清空set中所有的元素,没有返回值
- forEach(callback, [, thisArg]):通过forEach遍历set
- 另外Set是支持
for of的遍历的
8.3 Set和数组的比较
- Set是无序的,数组是有序的
- Set使用forEach只有value值,没有index
- Set只能使用
add方法添加元素,数组可以通过unshift和push添加元素- 数组的
unshift添加元素很慢
- 数组的
- Set查找元素用
has方法,相比较数组的includes速度会快
8.4 WeakSet
- 类似于Set,区别如下:
- WeakSet中只能存放对象类型,不能存放基本数据类型
- WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收
let obj1 = { name: "lilei" }
let obj2 = { name: "hanmeimei" }
let obj3 = { name: "jim" }
const weakSet = new WeakSet()
weakSet.add(obj1)
weakSet.add(obj2)
weakSet.add(obj3)
- WeakSet常见的方法:
- add(value):添加某个元素,返回WeakSet对象本身
- delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型
- has(value):判断WeakSet中是否存在某个元素,返回boolean类型
- WeakSet只是对对象的弱引用,如果遍历获取到其中的元素,有可能造成对象不能正常的销毁
- 所以存储到WeakSet中的对象是没办法获取的,不能够遍历,并且没有
size属性
- 所以存储到WeakSet中的对象是没办法获取的,不能够遍历,并且没有
- 应用
const pWeakSet = new WeakSet()
class Person {
constructor() {
pWeakSet.add(this)
}
running() {
if (!pWeakSet.has(this)) {
throw new Error("不能通过其他对象调用该类方法")
}
console.log("running~")
}
}
let p = new Person()
// 销毁
// p = null
p.running()
const runFn = p.running
runFn()
const obj = { run: runFn }
obj.run()
九、Map映射
9.1 Map基本使用
- Map用于存储映射关系
- 对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);
- 某些情况下可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key
const info = { name: "kobe" }
const info2 = { age: 24 }
// 1.对象类型的局限性: 不可以使用复杂类型作为key
// const obj = {
// address: "北京市",
// [info]: "哈哈哈",
// [info2]: "呵呵呵"
// }
// console.log(obj) // {address: '北京市', [object Object]: '呵呵呵'}
// 2.Map映射类型
const map = new Map()
map.set(info, "aaaa")
map.set(info2, "bbbb")
console.log(map) // Map(2) {{…} => 'aaaa', {…} => 'bbbb'}
9.2 Map常见方法和属性
- Map常见的属性:
- size:返回Map中元素的个数
- Map常见的方法:
- set(key, value):在Map中添加key、value,并且返回整个Map对象
- get(key):根据key获取Map中的value
- has(key):判断是否包括某一个key,返回Boolean类型
- delete(key):根据key删除一个键值对,返回Boolean类型
- clear():清空所有的元素
- forEach(callback, [, thisArg]):通过forEach遍历Map
- Map也可以通过
for of进行遍历
// map.forEach(item => console.log(item)) // 拿到的是值
// for...of遍历
for (const item of map) {
const [key, value] = item
console.log(key, value)
}
9.3 Map和对象的比较
- Map可以以任意类型为
key,Object只能以字符串为key - Map是有序的,Object是无序的
// Map是有序的
const m = new Map([
['key1', 'hello'],
['key2', 100],
['key3', { x: 100 }]
])
m.forEach((value, key) => console.log(key, value))
// Object是无序的
const obj = { 2: 20, 3: 30, 1: 10 }
console.log(Object.keys(obj)) // ['1', '2', '3']
- Map虽然有序,但是操作很快
const obj = {}
for (let i = 0; i < 1000 * 10000; i++) {
obj[i + ''] = i
}
console.time('obj find')
obj['5000000']
console.timeEnd('obj find') // obj find: 0.008056640625 ms
console.time('obj delete')
delete obj['5000000']
console.timeEnd('obj delete') // obj delete: 0.004150390625 ms
// Map 有多块
const m = new Map()
for (let i = 0; i < 1000 * 10000; i++) {
m.set(i + '', i)
}
console.time('map find')
m.has('5000000')
console.timeEnd('map find') // map find: 0.010986328125 ms
console.time('map delete')
m.delete('5000000')
console.timeEnd('map delete') // map delete: 0.009033203125 ms
9.4 WeakMap
- 类似于WeakMap,也是以键值对的形式存在的
- WeakMap的key只能使用对象,不接受其他的类型作为key
- WeakMap的key对对象的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象
- WeakMap常见的方法:
- set(key, value):在Map中添加key、value,并且返回整个Map对象
- get(key):根据key获取Map中的value
- has(key):判断是否包括某一个key,返回Boolean类型
- delete(key):根据key删除一个键值对,返回Boolean类型
- WeakMap也是不能遍历的,没有forEach方法,也不支持通过
for of的方式进行遍历 - 应用:Vue3响应式原理中用到
十、其他知识(后续补充)
- Proxy、Reflect
- Promise:后续会详细介绍
- ES Module模块化开发