小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
导言: ECMAscript 是什么
ECMAscript:是一个规范,js则是这个代码的规范,ECMA基本上每年都会更新一次,提出新的 js规范。目前2021年最新的版本是 12,不过目前最普及的还是 ES5。ES6因为更新了大量的规范,所以现在正在慢慢的普及中,几乎可以看到无论是中小大公司都要求会 ES6。ES6 之后每年更新的规范并不多,实际上说学 ES6 就是将 ECMA6 及之后新出的规范都学一遍。
一、 let var const 之间的区别
1. var 与 let 的区别
1) let 不存在变量提升(也就是无法预解析)
- var 存在变量提升:也就是运行时浏览器会提前声明所有变量,但不会进行赋值。也就说在打印使用 var 声明的不存在变量时不会报错。
例:
// var 变量预解析:实际上这段代码的执行过程:
// var a; ===> console.log(a); ===> a = 10; ===> console.log(10)
console.log(a) // 输出:undefined
var a = 10
console.log(a) // 输出:10
// let 无法预解析,执行 console.log(a) 时就会报错
// 因为这个变量还未声明,根本不存在
console.log(b) // 报错
let b = 10
console.log(b) // 输出:10
2)let 在同一个作用域下不能重新声明,只能重新赋值。
例:
// 在 var 声明变量时,如果重复声明同一个变量名,则第二个会覆盖第一个
var a1 = 1
var a1 = 2
console.log(a1) // 输出:2
// 但是在 let 里不行,会直接报错,在 let 中只能重新赋值而不能重新声明
let b1 = 1
let b1 = 2
console.log(b1) // 报错
3)作用域不同
- var:函数作用域
- let:块级作用域
块级作用域这个概念也是 ES6 新增的。
例:
// var 的函数作用域可以理解为全局作用域,只要声明了,在哪都可以用,那都存在。
// 也就是说在这个函数内,a2 和 if 里的 a2 会被视为同一个作用域,同一个变量。
// 也就是说,在 var 里,这里的两次声明都是对同一个变量进行声明
function f1(){
var a2 = 1 // 将这行注释,输出结果是:undefined 2 2
if(true){
console.log(a2) // 输出:1
var a2 = 2
console.log(a2)
}
console.log(a2) // 输出:1 2 2
}
f1() // 输出:2 2
// let 是块级作用域,b2 和 if 里的 b2 互不干扰
function f2(){
// 如果注释这一行,会报 b2 is not defined
let b2 = 1
if(true){
// 这里输出会报错
// console.log(b2)
let b2 = 2
console.log(b2)
}
console.log(b2)
}
f2() // 输出:2 1
2. const 与 var 的区别
1)const 声明的基本类型不可改变,但引用类型能够改变
- 基本类型:String Number 等等
- 引用类型:Array Object Set Map
例:
// const 声明一个不能更改的常量
const a3 = 1
a3 = 2 // 报错
// 但是 const 声明的数组与对象类型的数据能修改
const b3 = [1,2]
b3.push(3)
console.log(b3) // 输出:1 2 3
2)const 声明时必须给定初始值
例:
// const 与 let 和 var 不同,在声明时就必须给值,不能为 undefined
const z; // 报错
3. 实例
1)三个关于 var 和 let 的小实例,很容易把人搞昏头。
先思考,得出自己的答案,再去看答案和解析
// 1.
function let1(i){
let i = 100
console.log(i)
}
let1(6) // 输出什么?
// 2.
// var
for(var i = 0; i <= 10; i++){}
console.log(i) // 输出什么?
// let
for(let i = 0; i <= 10; i++){}
console.log(i) // 输出什么?
// 3.
// var
var arr = []
for (var i = 0; i < 5; i++) {
arr[i] = function(){
console.log(i)
}
}
arr[3]() // 输出什么?
// let
var arr = []
for (let i = 0; i < 5; i++) {
arr[i] = function(){
console.log(i)
}
}
arr[3]() // 输出什么?
2)题解
// 1.
function let1(i){ // 传入的值在函数内部相当于执行了:var i = 6
let i = 100 // 在 let 里同一个变量不能重复声明,函数的传参也是声明了一个变量。
console.log(i)
}
let1(6) // 报错
// 2.
for(var i = 0; i <= 10; i++){} // var 的作用域是函数作用域,也就是全局作用域
console.log(i) // 输出:10; 其在 for 里声明的变量,在 for 之外也能存在
// let
for(let i = 0; i <= 10; i++){} // let 块级作用越,里外声明的变量毫不干扰
console.log(i) // 报错; 因为在这个作用域内并没有声明过这个变量
// 3.
// var
var arr = []
for (var i = 0; i < 5; i++) { // 因为 var 是函数作用域,也就是全局的
arr[i] = function(){ // 其内部每次执行其实都是对同一个变量进行操作
console.log(i) // 所以执行的最终结果都是 5
} // 相当于这个 for 重新声明了 5 次同一个变量 i
}
arr[3]() // 输出:5;无论输出 0 还是 4 都是5
// let
var arr = []
for (let i = 0; i < 5; i++) { // let 锁声明的每个变量都毫不相关
arr[i] = function(){ // 各自有各自的作用域
console.log(i) // 相当于这个 for 声明了五个不同作用域的变量 i
}
}
arr[3]() // 输出:3
4. 总结:
总的来说,个人理解:var 声明全局变量;let 声明局部变量,const 声明常量。let 与 const 更多的是在弥补 var 声明变量时的不规范,更好的区别出了局部变量与全局变量以及不更修改变量之间的关系。使其能够更好的规范、语义化我们的代码。
知识点:
-
let 没有变量提升,无法预解析
-
let 是块级作用域
-
let 不能重复声明
-
const 声明的基本类型数据为常量,不能修改。但引用类型可修改
-
const 在声明时必须赋予初始值
二、 箭头函数的定义与普通函数的区别
ES6 新增的箭头函数,可以简单理解为函数的语法糖,它简化了我们的函数定义及代码量。但又与普通函数有着极大的区别。
1. 箭头与普通函数定义的区别
这里就可以看出,箭头函数在定义时就省去了 function 与 return,简化了我们的代码量。
// 只有一个参数时
// 箭头函数
let f = v => v
// 普通函数
var a = function(n){
return n
}
// 带有执行体的函数
// 箭头函数
let f1 = v => {
if(v >= 5){
// ...
}
}
// 普通函数
var a1 = function(n){
if(n >= 5){
// ...
}
}
// 无参数写法
// 箭头函数
let f2 = () => 123
// 普通函数
var a2 = function(){
return 13
}
// 多参数时
// 箭头函数在有多个参数时需要加上括号
let f3 = (v1, v2) => v1 + v2
// 普通函数
var a3 = function(n1, n2){
return n1 + n2
}
// 返回结果是对象时
// 箭头函数在返回对象是需要小括号加大括号表明返回结果是对象
let f4 = (v1, v2) => ({v1: 1, v2: 2})
// 普通函数
var a4 = function(n1, n2){
return {n1: n1, n2: n2}
}
2. 箭头函数与普通函数的区别
1)箭头函数不能作为构造函数
// 箭头函数
let Fun = name => {
this.name = '你好'
}
// 报错:Fun is not a constructor
let f = new Fun()
// 普通函数
var A = function(name){
this.name = '你好'
console.log(this.name)
}
// 输出:你好
var aName = new A()
2) 箭头函数没有原型对象
// 箭头函数
let fun1 = () => {}
// 报错:Identifier 'fun2' has already been declared
fun1.prototype
// 普通函数
var a1 = function(){}
// 不会报错
a1.prototype
3) 箭头函数没有 arguments 对象
// 箭头函数
let fun2 = () => {
console.log(arguments)
}
// 报错
fun2(1, 2, 3, 4, 5)
// 普通函数
function a2(){
console.log(arguments)
}
// 输出:[ 1, 2, 3, 4, 5 ]
a2(1, 2, 3, 4, 5)
4)箭头函数没有绑定 this
// 普通函数
var s = 'abc' // 全局变量
var obj = { // 定义一个对象
s: 'def', // 对象中的属性
get: function(){ // 对象中的方法
console.log(this.s) // 对象中的方法默认绑定了这个作用域的 this
} // 也就是说,这个 this 指向这个对象的属性 s: 'def
}
obj.get() // 输出:def
// 箭头函数
let s = 'abc' // 全局变量
let obj = { // 定义一个对象
s: 'def', // 对象中的属性
get: () => { // 对象中的方法
console.log(this.s) // 而箭头函数并不会绑定this
} // 也就是说这个 this 并不会指向 s: 'def'
} // 因为这个 this 没有绑定 obj 对象
// 而是在使用时,才会在使用它的上下文去捕获 this 值
obj.get() // 输出:abc 因为箭头函数没有绑定 this
// 所以它会去捕获,它所在的上下文的 this 值
// 而 obj 的上下文变量是 s 即 window 全局对象,所以会输出:abc
3. 总结
其实就可以简单的将箭头函数当作是一个普通函数的简化版(语法糖)。省去了代码量的同时也失去了部分函数的特性。
知识点:
-
箭头函数不能作为构造函数
-
箭头函数没有原型对象
-
箭头函数没有 arguments 参数集
-
箭头函数没有绑定 this
-
箭头函数不能当做 Generator 函数,不能使用 yield 关键字
三、 新数据类型 Set Map
1. Set
1)set 集合的使用
// 声明 set
var set = new Set()
// set 添加数据
set.add(1)
// set 支持链式语法
set.add(2).add(3)
console.log(set) // 输出:Set(3) { 1, 2, 3 }
// set 的值具有唯一性,也就是说 set 自带去重
var set1 = new Set([1, 2, 3, 1, 2, 3])
console.log(set1) // 输出:Set(3) { 1, 2, 3 }
// 返回 set 的个数
const set2 = new Set([42, 'forty two'])
console.log(set2.size) // 输出:2
// 取出 set 的值
const set3 = new Set(['0', 1, {}]);
const setIter = set3[Symbol.iterator]()
console.log(setIter.next().value) // 输出:"0"
console.log(setIter.next().value) // 输出:1
console.log(setIter.next().value) // 输出:Object { }
2)利用 Set 实现数组去重功能
因为 Set 返回的并不是一个数组,所以需要对 set 进行类型转换,将其转换成数组类型。
// 虽然 set 能够去重,但 set 的返回结果并不是数组
const arr = [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
let set4 = new Set(arr)
console.log(set4) // 输出:Set(3) { 1, 2, 3 }
// 1. 第一种转换方式
const arr2 = Array.from(set3)
console.log(arr2) // 输出:[1, 2, 3]
// 2. 第二种转换方式,使用 ES6 的展开运算符
const arr3 = [...new Set(set3)]
console.log(arr3) // 输出:[1, 2, 3]
2. Map
1)Map 映射的使用
map 类似于对象,也是键值的方式存取数据,但是它的键可以是任何其他的类型
// 声明一个 map
const map = new Map()
// map 添加数据,且也支持链式语法
map.set('name', '你好').set('age', 18)
// 将 obj 这个对象作为键并添加数据 123
var obj = {id: 1}
map.set(obj, 123)
// map 取值的方式,可以写多个不报错,但只会输出第一个
console.log(map.get(obj, 'name')) // 输出:123
// 输出:Map(3) { 'name' => '你好', 'age' => 18, { id: 1 } => 123 }
console.log(map)
// 第二种定义方式,以二维数组形式进行添加
let b
var map2 = new Map([['a', 1], [b, 2], [1, 3]])
console.log(map2) // 输出:{ 'a' => 1, undefined => 2, 1 => 3 }
2)循环输出 Map 与 Object 的区别
// 平常我们遍历输出对象的方式
var obj1 = {
id: 1,
name: '你好'
}
// 使用 for...in 循环
for(let key in obj1){
// 获取对象的 key 和 值
console.log(key, obj1[key]) // 输出:id 1
} name 你好
// 使用for...of 遍历 map
var map3 = new Map([['id', 2], ['name', '你好好'], ['age', 16]])
for(let [k, v] of map3){
console.log(k, v) // 输出:id 2
} name 你好
age 16
3)将 Map 转为对象类型
var map4 = new Map([['id', 3], ['name', '他好'], ['age', 17]])
// 利用 for 循环取值将 map 取出来放到对象里
var obj2 = {}
for(let [k, v] of map4){
obj2[k] = v
console.log(obj3) // 输出:{ id: 3 }
} { id: 3, name: '他好' }
{ id: 3, name: '他好', age: 17 }
3. 总结
个人感觉,Set 像是数组的升级版,自带去重。Map 则是对象的升级版,扩展了对象的键只能是字符串的限制。
知识点:
-
set.add() 添加数据,set.size() 获取长度,Set 支持链式语法
-
Set 成员值是唯一的,自带去重功能
-
ES5 Set 转数组:Array.from(set) ;ES6 Set 转数组:[...new Set(set)]
-
map.set 添加数据,map.get() 取值,map.size() 获取长度,且支持链式语法
-
Map 还可以以二维数组的形式进行定义:new Map([['a', 1], [b, 2], [1, 3]])
-
map 以 for(let [k, v] of map) 的形式取出所有值
Map 和 Object 的区别
- Object 对象有原型,Map 没有。
- Object 的 key 只能为 String 和 Symbol,Map 的 key 能为任意类型数据。
四、 解构赋值
解构方法 解构的目的时让我们 ES5 的代码更加简洁、清晰,并减少我们的代码量。
解构的定义:从数组或对象中提取值,对变量进行赋值
1. 数组解构
1)数组解构的使用
// 其实就相当于是一个模式匹配:左边是变量,右边是值
let [a, b, c] = [1, 2, 3]
// ES5 的两种写法
var d = 1
var e = 2
var f = 3
var g = 1, h = 2, i = 3
console.log(a, b, c, d, e, f, g, h, i) // 输出:1 2 3 1 2 3 1 2 3
// 数组解构更多类型数据
let [a1, b1, c1, d1] = [1, [2, 3, 4], {id: 1}, true]
console.log(a1, b1, c1, d1) // 输出:1 [ 2, 3, 4 ] { id: 1 } true
2)数组的默认值处理
// 默认值处理
var arr = [10, 20, 30]
// 在赋值的时候会先判断 arr 数组相应位置的值是否存在,如果不存在,则赋值后面的数据
// 也就是说,在赋值时给了一个 undefined 的情况下
// 浏览器不会报错,而是会将默认值赋给变量
// 这种写法可以让我们的代码更加严谨和规范
// 数组的 [0] 存在,所以 s 不会被赋予默认值
var s = arr[0] || 1
// 数组的 [3] 不存在,所以 v 会被赋予默认值 2
var v = arr[3] || 2
console.log(s, v) // 输出:10 2
// 解构数组的默认值
var arr1 = [10, 20, 30]
// 在声明的时候就给其加上默认值,如果左边的变量匹配不到右边的值
// 就不会报 undefined,而是会被赋上默认值
let [s2 = 1, v2 = 2, s2 = 3, v2 = 4] = arr1
// 前面三个变量都对应了 arr1 的值
// 而最后一个变量在 arr1 里不存在,所以会被赋上默认值 4
console.log(s2, v2, s2, v2) // 输出:10 20 30 4
// 注:默认值只在值为 undefined 的时候生效
let [x = 1, y = 2 ] = [undefined, null]
console.log(x, y) // 输出 1 null
2. 对象解构
1)对象结构的使用
// 2. 对象解构
var obj = {
id: 1,
name: '你好',
age: 19,
arr: [1, 2]
}
// 对象解构时,不按顺序,按名称赋值
// 且可以再解构过程加上新的变量
let {arr, id, name, age, dog = '狗狗'} = obj
console.log(id, name, age, arr, dog) // 输出:1 你好 19 [ 1, 2 ] 狗狗
// 以往写法,需要单独取出对应的 obj 的值再去赋值
var id = obj.id, name = obj.name, age = obj.age
// 但是解构时不能将对象赋为新变量
let { ids = id } = obj
console.log(ids) // 输出:id is not defined
// 对象结构使用 function 获取对象的值
var obj2 = {
a: 1,
b: 2
}
// v: 第一个形参, { a, b } 则是对象解构的用法
function fun(v, {a, b}){
console.log(v, a, b)
}
// 将对象传进去的同时进行解构赋值
fun(3, obj2) // 输出:3 1 2
2)对象结构的别名与默认值
// 别名: 变量:变量名
// x 是一个匹配模式,a才是变量
let {x: a, y} = {x: 10, y: 20}
// 这里输出 x 会报错:x is not defined
console.log(y, a) // 输出:20 10
// 实际上 let {a, b} = {a: 1, b: 2} 是 let {a: a, b: b} = {a: 1, b: 2} 的简写方式
// 对象结构默认都有别名,不过是在原本名与别名相同情况下可以简写省去别名
let {a, b} = {a:1, b: 2}
console.log(a, b) // 输出:1 2
// 别名加上默认值写法
let {x: a = 1, y = 1, z: u = 1, w: q = 1} = {x: 2, y: 2, z: undefined}
// 这里因为给 z 赋值 undefined 所以 z 会被赋上默认值 1,
// 而 w 没有被赋值,所以也会被赋上默认值 1
console.log(a, y, u, q) // 输出:2 2 1 1
3. 总结
解构赋值,可以说是简化了我们从数组与对象中的提取值步骤,可以让我们在数组与对象中取值时让代码更加简洁、清晰的同时减少了我们的代码量。美化了代码整体的观赏感。
知识点:
-
数组解构:let [a, b, c, d, e] = [1, {id: 2}, true, undefined, [3, 4, 5], null];解构的值也可以是变量
-
数组默认值:let [a1 = 1,b2 = 1] = [2];// 没有给值赋值时,会使用默认值代替;默认值只在值为 undefined 时生效
-
let {id, name, age = '男'} = {obj // 对象解构不按顺序,只按名字赋值;对象解构过程可以声明新的且对象中不存在的变量
-
对象解构中的别名:变量: 变量名:let {x: a, y} = {x: 10, y: 20};对象解构的别名变量名与变量名相同时可以省略为:{x: a} => {x}
-
对象解构的别名 + 默认值写法:let {x: a = 1, y: s = 1, z =1} = {2, 2, 2}
-
使用函数解构对象:let fun = (v, {a, b}) => {console.log(v, a, b)}; fun(1, obj)
五、使用 ES6 制作一个留言列表
也不难,主要是简单理解下使用 ES6 新特性实战。留言表没有写 CSS 样式,写的过程封装了 querySelector 选择器,还用到了 map 和 对象结构
1. HTML 代码
// 并没有 css 样式
<div class="box">
<p class="user">昵称:</p>
<input id="user" type="text" placeholder="请输入名称">
<p class="content">留言:</p>
<textarea name="content" id="content" cols="30" rows="10" placeholder="请输入留言"></textarea>
<br>
<input class="submit" type="submit" value="提交留言">
<div class="contentList">
<div class="top">留言列表</div>
<div class="bottom">
<ul id="list">
</ul>
</div>
</div>
</div>
2. JavaScript 代码
<script type ="text/javascript">
// 使用原生 js 制作留言提交
window.onload = function(){
// 存放留言列表
const map = new Map()
// 选择器封装
let cy = name => document.querySelector(name)
// 使用封装的选择器找到元素并绑定点击事件
cy(".submit").onclick = () => {
// 使用解构赋值拿到表单的值
let [name, content] = [cy("#user").value, cy("#content").value]
if(name == '' || content == ''){
// 提示
alert('昵称与留言不能为空')
// 并返回
return false
}
// 输入都不为空时,加入留言列表
map.set(name, content)
// 调用插入列表函数
listShow()
}
// 将留言插入列表
let listShow = () => {
// 存放留言
let str = ""
// 循环取出留言
for(let [k, v] of map){
// 存入留言
str += `<li>${k}<span>说:</span>${v}</li>`
}
// 插入页面
cy("#list").innerHTML = str
}
}
</script>
效果:
3. 总结
其实也没啥好总结的,主要是利用了 map 与对象解构存取数据,过程还使用箭头函数封装了选择器,达到减少我们总体代码量的效果。
再使用封装的选择器和对象解构快速拿到输入的数据,在进行判断不为空后使用 map.set() 将数据放入到 map 中并将 map 中的数据循环取出插入到页面之中。
六、Class 类
ES6新增的声明类的一种方式,使 js 更加贴近了其他语言。
// 声明类
class Person{
// 构造器
constructor(){
// 每次实例化类都会执行这里的代码
}
// 添加属性
name = '张三'
// 添加方法
getName(){
console.log(this.name)
}
}
// 实例化类
let p = new Person()
// 输出:张三
console.log(p.name)
// 输出:张三
Person.getName()
1. 静态方法的定义
Static 静态定义方法:使用静态添加的方法,智能通过类本身调用,不能用实例对象调用。
class Person{
constructor(){
// ...
}
// 定义在类本身上
static locate(){
console.log('class');
}
}
// 实例化
let p = new Person()
// 报错
p.locate()
// 输出:class
Person.locate()
2. 类的继承
Extends 继承:ES6 Class 类的继承。
super():引用原型,也就是使用父类的属性和方法
// 声明一个父类
class Person{
constructor(){
this.name = '你好'
}
// 添加一个静态方法
static name(name){
console.log(name)
}
}
// 声明子类继承父类
class Student extends Person{
// 构造器指向本类的构造者,这里会指向父类 Person,在没有父类的情况下会指向自己
constructor(){
// 通过 super() 使用父类也就是原型上的属性及方法
super()
console.log('我好')
}
// 使用父类的静态方法
static getName(name){
return super.name(name)
}
}
// 输出:我好
let s = new Student()
// 输出:你好
console.log(s.name)
// 输出:大家好
Student.getName('大家好')
3. 声明只可被继承类
使用 new.target 实现,声明一个只可以被子类继承,但自身不能被实例化的类。
class Person{
constructor(name){
// 判断 Person 是否被实例化了
if (new.target === Person){
throw new Error("Person 不能实例化")
}
this.name = name
}
}
// 子类继承父类
class Student extends Person{
constructor(name){
super(name)
console.log(name)
}
}
// 输出:你好
let s = new Student("你好")
// 输出:Error: Person 不能实例化
let p = new Person("我好")
七、模块化(ES Module)
ES6 模块化,import 导入 export 导出与 Common.js、CMD.js、AMO.js 不同。属于纯静态加载外部文件。也称为:编译时加载。
1. import 导入
// 导入外部文件变量
import {a, _b ,c} from './profile'
// 也可以使用 as 关键字重命名导入的变量
import {stream1 as firstVal} from './profile'
1.1 模块的整体加载
导入模块时可以使用 * 来指定对象,将输出值都加载到这个对象上。(Echarts 也是这种导入法)
// 指定 circle 对象
import * as circle from './module1'
// 使用导入的方法
circle.foo()
circle.bar()
// 因为 ES6 的模块加载是完全静态的,所以在运行时是不允许改变的
// 下面两行代码都会报错
circle.foo = 123
circle.bar = function(){}
1.2 import() 方法
因为 import 是静态加载的,所以必须将 import 导入语句放在代码的最上面,否则就会报错。但是可以使用 import() 函数实现异步、动态加载。
// 获取 DOM 元素存放加载结果
const main = document.querySelector('main')
// 使用变量存储加载指定的模块名
let someVariable = 'profile'
// 使用 import() 函数加载外部文件
import(`./section-modules/${someVariable}.js`)
// 加载成功时
.then(module => {
module.loadPageInto(main)
}, err => {
// 加载失败时
main.textContext = err.message
})
2. export 导出
// 声明两个变量和一个常量
var a = '123';
const _b = '2323'
let c = '2222'
// 导出 两个变量和一个常量
export {a, _b, c}
2.1 使用 as 关键字重命名
var a = '123'
const _b = '2323'
let c = '2222'
// 导出的同时重命名
export {
a as stream1,
_b as stream2,
c as stream3
}
Tips:
- export 语句输出的接口是对应值的引用,也就是一种动态绑定关系,通过该接口可以获取模块内部实时的值。
对比 CommonJS规范:CommonJS 模块输出的是值的缓存,不存在动态更新。 - export 命令规定要处于模块顶层,一旦出现在块级作用域内,就会报错,import 同理。
3. export default 默认输出
// 导出默认内容
export default function(){
console.log('123')
}
// 相当于
function a(){
console.log('123')
}
export {a as default};
八、Promise 异步处理方案
这里建议看我其他的文章,有专门讲述 js 异步的
九、Symbol
ES6 新增的第七种数据类型。Symbol 表示独一无二的值。
// 声明无参数 Symbol
let s1 = Symbol()
let s2 = Symbol()
// 输出:false
console.log(s1 == s2)
// 声明有参 Symbol;Tips:这里的 test 只是 Symbol 的一个描述
// 并非代表两个 Symbol 的参数都是 test
let s1 = Symbol('test')
let s2 = Symbol('test')
// 输出:false
console.log(s1 == s2)
1. Symbol 的特性
- typeof 和 instanceof 数据类型检查结果。
let a = Symbol()
let b = Symbol()
// 输出:"symbol" false
console.log(typeof a, a instanceof Symbol)
// 因为 Symbol 是原始类数据,所以在使用 instanceof 时会报 false
- Symbol 不能使用 new 运算符。
- Symbol 的参数是一个对象时,会自动调用该对象的 toString 方法,将该对象转换为字符串。
- Symbol 值不能与其他类型的进行运算
2. .for() 与 .keyFor() 方法
Symbol.for() 方法:使用时回去查询全局注册表,如果关键字已存在则返回字符串实例,如果不存在,则创建一个新的 Symbol。
// Symbol.for() 函数的执行过程::查找注册表--->注册表中无--->创建;有--->使用
let globalSymbol = Symbol.for('test')
// 输出:Symbol(test)
console.log(globalSymbol)
let test = Symbol.for('test')
// 输出:true
console.log(globalSymbol === test)
Symbol.keyFor() 方法:在全局注册表中查找 Symbol,查找到了返回属性值,无属性值返回 undefined如果没找到该 Symbol 则会报错:TypeError: 查找目标 is not a symbol。
// 注册 Symbol
let test1 = Symbol.for('test1')
let test2 = Symbol('test2')
// 输出:test1
console.log(Symbol.keyFor(test1))
// 输出:undefined
console.log(Symbol.keyFor(test2))
// 输出:TypeError: 1233 is not a symbol
console.log(Symbol.keyFor(1233))
十、Proxy(代理) 和 Reflect(反射)
- Proxy 对对象进行代理,能够代理对象的某些操作,也可以理解为拦截目标对象的操作。
- Vue3 即是改用的 Proxy 实现数据响应式。
- Reflect 是 ES6 为了操作对象二提供的新 API。一些 Object 上的方法,例如 Object.defineProperty() ,Reflect 都有,并且 Reflect 对象上的方法更为合理。简而言之,就是 Object 对象的进化版,在未来 Reflect 会慢慢代理 Object 的方法。
- 为了方便在 Proxy 中使用 Reflect,所以 Reflect 对象的方法与 Proxy 是一一对应的。
10.1 使用 Proxy 实现对象的代理
// 创建一个对象
const obj = {
name: '张三',
age: 18
}
// 代理这个对象
const proxy = new Proxy(obj, {
// 代理 get() 设置查值操作
// target:对象本身
// prop:查找属性
get(target, prop){
// 如果要查找的属性不存在
if(!prop in target){
// 返回自定义报错信息
return new Error('此属性不存在!')
}
// 使用 return 方式返回值,并添加自定信息
return '取值:' + target[prop]
},
// 代理 set() 设置值操作
// target:对象本身
// prop:查找属性
// value: 新值
set(target, prop, value){
// 直接修改
target[prop] = '旧值:' + target[prop] + ';新值:' + value
}
})
// 输出:取值:张三
console.log(proxy.name,)
proxy.age = 12
// 输出:取值:原值:18;被修改后值:12
console.log(proxy.age)
10.2 使用 Reflect 配合 Proxy 代理使用
// 创建一个对象
const obj = {
name: '张三',
age: 18
}
// 代理这个对象
const proxy = new Proxy(obj, {
// 代理 get() 设置查值操作
get(target, prop, context){
// 如果要查找的属性不存在
if(!prop in target){
// 返回自定义报错信息
return new Error('此属性不存在!')
}
// 使用反射来返回值
return Reflect.get(target, prop, context) // 相当于执行了 target[propr]
},
// 代理 set() 设置值操作
set(target, prop, value){
// 使用 Reflect 反射来修改值
return Reflect.set(target, prop, value) // 相当于执行了 target[prop] = value
}
})
// 输出:张三
console.log(proxy.name)
proxy.age = 12
// 输出:12
console.log(proxy.age)
10.3 使用 Proxy 和 Reflect 实现观察者模式
Observer 观察者模式:
- Subject:被观察者
- Observer:观察者
// 使用 Set 存储观察者列表
const queuedObservers = new Set()
// 添加方法
const observe = fn => queuedObservers.add(fn)
// 添加订阅者
const observable = obj => new Proxy(obj, {set})
// 执行发布任务
function set(target, key, value, receiver){
// 使用反射执行
const result = Reflect.set(target, key, value, receiver)
// 将方法遍历出来一一执行
queuedObservers.forEach(observer => observer())
// 返回执行结果
return result
}
// 添加订阅者 person
const person = observable({
name: '张三',
age: 20
})
// 输出函数
function print(){
console.log(`${person.name}, ${person.age}`)
}
// 添加方法
observe(print)
// 触发订阅器
person.name = '李四'
// 触发订阅器
person.name = '张三'
\