babel,字面量的增强,解构,let/const,函数参数,Symbol,集合【Set,Map,WeakSet,WeakMap】,Proxy,Reflect,响应式原理及实现
1. babel
让代码向下兼容ES5,ie浏览器
1.1. ES6
1 | class Person{ |
1.2. Person
1 | function _classCallCheck(instance, Constructor) { |
1.3. Student
1 | function _setPrototypeOf(o, p) { |
2. 字面量的增强
- ES6中对
对象字面量进行了增强,称之为 Enhanced object literals(增强对象字面量) - 字面量的增强主要包括下面几部分
- 属性的简写:Property Shorthand
- 方法的简写:Method Shorthand
- 计算属性名:Computed Property Names
1 | var name = 'why'; |
3. 解构Destructuring
ES6中新增了一个从数组或对象中方便获取数据的方法,称之为解构Destructuring
可以划分为:数组的解构和对象的解构
3.1. 数组的解构
3.1.1. 基本解构过程
1 | var names=["abc","cba","nba"]; |
3.1.2. 顺序解构
1 | var names=["abc","cba","nba"]; |
3.1.3. 解构出数组
1 | var names=["abc","cba","nba"]; |
3.1.4. 默认值
1 | var names=["abc","cba","nba"]; |
3.2. 对象的解构
3.2.1. 基本解构过程
1 | var obj = { |
3.2.2. 任意顺序
1 | var obj = { |
3.2.3. 重命名
1 | var obj = { |
3.2.4. 默认值
1 | var obj = { |
3.3. 解构的应用场景
- 比如在开发中拿到一个变量时,自动对其进行解构使用;
- 比如对函数的参数进行解构;
1 | var obj = { |
4. let/const
4.1. var/let/const
在ES5中声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const
let关键字:从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个
变量const关键字
const关键字是constant的单词的缩写,表示
常量、衡量的意思它表示保存的数据一旦被赋值,就不能被修改
1
2
3const name = "Matt";
// TypeError: Assignment to constant variable.
name = "Hello";但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容
1
2
3
4
5const obj = {};
obj.name = "Matt";
obj.name = "Hello";
// { name: 'Hello' }
console.log(obj);
注意
- let、const不允许重复声明变量
4.2. 作用域提升
let、const和var的另一个重要区别是作用域提升
var声明的变量是会进行作用域提升
1
2
3// undefined
console.log(foo);
var foo = 'why';但是如果使用let/const声明的变量,在声明之前访问会报错
1
2
3// ReferenceError: Cannot access 'foo' before initialization
console.log(foo);
let foo = 'why';
是不是foo变量只在代码执行阶段被创建?
事实上并不是这样的,ECMA262对let和const的描述显示
https://ecma262.docschina.org/#sec-let-and-const-declarations
let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.
这些变量会被创建在包含他们的词法环境被实例化时,但是
不可以访问它们,直到词法绑定被求值
从上面可以看出,在执行上下文的词法环境创建出来的时候,变量事实上已经被创建了,只是这个变量是不能被访问的
那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?
事实上维基百科并没有对作用域提升有严格的概念解释,那么从字面量上理解
作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么可以称之为作用域提升- 在这里,它虽然被创建出来了,但是不能被访问,不能称之为作用域提升
所以个人观点是let、const没有进行作用域提升,但是会在解析阶段被创建出来
4.3. 变量存储
在全局通过var来声明一个变量,事实上会在window上添加一个属性
1
2
3var name = "Matt";
// Matt
console.log(window.name);但是let、const是不会给window上添加任何属性的
那么这个变量是保存在哪里呢?
先回顾一下最新的ECMA标准中对执行上下文的描述
Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the variable object.
每一个执行上下文会被关联到一个变量对象,在源代码中的便利和函数声明会被作为属性添加到VO中。对于函数来说,参数也会被添加到VO中
Every execution context has an associated VariableEnvironment. Variables and functions declared in ECMAScript code evlauated in an execution context are added as bindings in that VariableEnvironment’s Environment Record. For function code, parameters are also added as bindings to that Environment Record.
每一个执行上下文会关联到一个
变量环境(VariableEnvironment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中也就是说声明的变量和环境记录是被添加到变量环境中的
但是标准有没有规定这个对象是window对象或者其他对象呢?
其实并没有,JS引擎在解析的时候,其实会有自己的实现
比如v8中其实是通过VariableMap的一个hashmap来实现它们的存储
而window对象是早期的GO对象,在最新的实现中其实是浏览器添加的全局对象,并且一直保持window和var之间值的相等性
https://github.com/v8/v8/blob/master/src/ast/scopes.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// A hash map to support fast variable declaration and lookup.
class VariableMap : public ZoneHashMap {
public:
explicit VariableMap(Zone* zone);
VariableMap(const VariableMap& other, Zone* zone);
VariableMap(VariableMap&& other) V8_NOEXCEPT : ZoneHashMap(std::move(other)) {
}
VariableMap& operator=(VariableMap&& other) V8_NOEXCEPT {
static_cast<ZoneHashMap&>(*this) = std::move(other);
return *this;
}
Variable* Declare(Zone* zone, Scope* scope, const AstRawString* name,
VariableMode mode, VariableKind kind,
InitializationFlag initialization_flag,
MaybeAssignedFlag maybe_assigned_flag,
IsStaticFlag is_static_flag, bool* was_added);
V8_EXPORT_PRIVATE Variable* Lookup(const AstRawString* name);
void Remove(Variable* var);
void Add(Variable* var);
Zone* zone() const { return allocator().zone(); }
};
4.4. 块级作用域
4.4.1. var块级作用域
- var只会形成两个作用域:全局作用域和函数作用域
- var没有块级作用域
1 | // 全局作用域 |
4.4.2. let/const块级作用域
- 在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的
- 但是函数拥有块级作用域,外面依然是可以访问的,这是因为引擎会对函数的声明进行特殊的处理,允许像var那样进行提升
1 | { |
4.4.3. 嵌套使用
- js引擎会记录用于变量声明的标识符及其所在的块作用域
- 只要同一个块中没有重复声明就不会报错
1 | var name = "Matt"; |
4.5. 块级作用域的应用
4.5.1. if语句代码
1 | if(true){ |
4.5.2. switch语句代码
1 | var color = "red"; |
4.5.3. for语句代码
- var声明的迭代变量会渗透到循环外部
- 在之后执行超时逻辑时,所有的j都是同一个变量,输出的都是同一个最终值
- let声明的迭代变量的作用域仅限于for循环块内部
1 | for (var j = 0; j<10; j++) { |
4.5.4. 按钮点击事件
- 按钮点击事件function的上层作用域是全局
- 当全局查找i时,i的值已经是btns.length,所以无论点击哪个按钮,控制台输出的信息不会改变
1 | const btns = document.querySelectorAll("button"); |
- 改用立即执行函数
- 此时点击事件函数的上层作用域是function(n),n的值因为传入的参数值不同而不同
1 | const btns = document.querySelectorAll("button"); |
- 等价于使用let,现在有了块级作用域
- 当点击事件函数查找上层作用域时,找到的是块级作用域for语句,其中的j值循环执行而不同
1 | const btns = document.querySelectorAll("button"); |
4.6. 暂时性死区
- 它表达的意思是在一个代码中,使用let、const声明的变量,在声明之前,变量都是不可以访问的
- 这种现象称之为 temporal dead zone(暂时性死区,TDZ)
1 | var foo = "foo"; |
- 在解析代码时,js引擎注意出现到块中的let声明变量foo,在声明之前不能以任何方式来引用未声明的变量
- 在此阶段引用任何后面才声明的变量会抛出ReferenceError
4.7. 不能混用
- var和let并不是不同类型的变量,只是指出变量在相关作用域如何存在
1 | var name; |
4.8. var/let/const的选择
对于var
- var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题
- 其实是JavaScript在设计之初的一种语言缺陷
- 当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解
- 但是在实际工作中,可以使用最新的规范来编写,也就是不再使用var来定义变量了
对于let、const
- 是目前开发中推荐使用的
- 优先推荐使用const,这样可以保证数据的安全性不会被随意的篡改
- 只有当明确知道一个变量后续会需要被重新赋值时,这个时候再使用let
- 这种在很多其他语言里面也都是一种约定俗成的规范,尽量遵守这种规范
5. 模板字符串
在ES6之前,将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的(ugly)
ES6允许使用字符串模板来嵌入JS的变量或者表达式来进行拼接
- 首先,使用 `` 符号来编写字符串,称之为模板字符串
- 其次,在模板字符串中,我们可以通过
${expression}来嵌入动态的内容
1
2
3
4
5
6
7
8
9
10
11
12
13const uname = "why";
const age = 18;
const height = 1.88;
// my name is why, age is 18, height is 1.88
console.log(`my name is ${uname}, age is ${age}, height is ${height}`);
// 成年人? 是
console.log(`成年人? ${age>=18?'是':'否'}`);
function foo(){
return "function is foo";
}
// my function is function is foo
console.log(`my function is ${foo()}`);模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)
模板字符串会被拆分
第一个元素是数组,是被模块字符串拆分的字符串组合
后面的元素是一个个模块字符串传入的内容
返回值是对模版字面量求值得到的结果
1
2
3
4
5
6
7
8
9
10
11
12function zipTage(strings,...expressions){
console.log(strings,expressions);
return "结果为 "+strings[0] + expressions.map((e,i)=>`${e}${strings[i+1]}`).join('');
}
var uname = "uname";
var address = "Beijing";
var message = zipTage`Hello ${uname}, from ${address}`;
// [ 'Hello ', ', from ', '' ] [ 'uname', 'Beijing' ]
// 结果为 Hello uname, from Beijing
console.log(message);
6. 函数参数
6.1. 默认参数
6.1.1. 介绍
- 在ES6之前,编写的函数参数是没有默认值的
- 如果在编写函数有下面的需求
- 传入了参数,那么使用传入的参数
- 没有传入参数,那么使用一个默认值
1 | function foo(){ |
6.1.2. 默认值与解构
1 | // 写法一 |
6.1.3. 默认值顺序
- 参数的默认值通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的)
- 但是JavaScript允许不将其放到最后,但是意味着还是会按照顺序来匹配
- 另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内
1 | function foo(x=20,y,z){ |
6.2. 剩余参数
6.2.1. 介绍
ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中
- 如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
那么剩余参数和arguments有什么区别呢?
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参
arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作- arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供 并且希望以此来替代arguments的
剩余参数必须放到最后一个位置,否则会报错
1 | function foo(m,n,...args){ |
6.2.2. 展开语法
展开语法(Spread syntax)
- 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开
- 还可以在构造字面量对象时, 将对象表达式按key-value的方式展开
展开语法的场景
- 在函数调用时使用
- 在数组构造时使用
- 在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性
1 | // 函数调用时使用 |
6.2.3. 展开运算符是一种浅拷贝
obj中friends属性存储的是原来的地址值,指向原来的对象
1 | const info = { |

7. 数值的表示
1 | const a = 100; |
在ES2021新增特性:数字过长时,可以使用_作为连接符
1 | const e = 100_000_000; |
8. Symbol
8.1. 介绍
Symbol是ES6中新增的一个基本数据类型,翻译为符号
为什么需要Symbol?
- 在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突
- 比如原来有一个对象,希望在其中添加一个新的属性和值,但是在不确定它原来内部有什么内容的情况下, 很容易造成冲突,从而覆盖掉它内部的某个属性
- 比如在讲apply、call、bind实现时,给其中添加一个fn属性,如果它内部原来已经有fn属性了呢?
- 比如开发中使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉
Symbol就是为了解决上面的问题,用来生成一个
独一无二的值- Symbol值是通过Symbol函数来生成的,生成后可以作为属性名
- 也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值
Symbol即使多次创建值也是不同的
- Symbol函数执行后每次创建出来的值都是独一无二的
- 也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性
1 | const s1 = Symbol(); |
8.2. Symbol作为属性名
使用Symbol在对象中表示唯一的属性名
1 | const s1 = Symbol(); |
8.3. 相同值的Symbol
如果想创建相同的Symbol应该怎么来做呢?
- 使用Symbol.for方法。用参数作为键,在全局符号注册表中创建并重用符号
- 并且通过Symbol.keyFor方法来获取对应的key
1
2
3
4
5
6
7
8
9
10
11
12
13
14const sy1 = Symbol.for("abc");
const sy2 = Symbol.for("abc");
// true
console.log(sy1 === sy2);
const key = Symbol.keyFor(sy1);
// abc
console.log(key);
// Symbol(abc)
console.log(sy1);
const sy3 = Symbol.for(key);
// true
console.log(sy2 === sy3);Symbol.keyFor()查询全局注册表,如果查询的不是全局符号,返回undefined
1
2
3let s = Symbol('abc');
// undefined
console.log(Symbol.keyFor(s));
9. 集合
9.1. Set
9.1.1. 介绍
在ES6之前,存储数据的结构主要有两种:数组、对象
在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap
Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是
元素不能重复创建Set需要通过Set构造函数(暂时没有字面量创建的方式)
Set有一个常用的功能就是给数组去重
1 | // 创建set对象 |
去重中的对象问题
1 | const s1 = new Set(); |
数组中元素去重后放入新数组
1 | const arr = [10,11,10,12]; |
9.1.2. 常用方法
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的遍历的
1 | // 3 |
9.2. WeakSet
9.2.1. 介绍
- 和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构
- 那么和Set有什么区别呢?
- 区别一:WeakSet中
只能存放对象类型,不能存放基本数据类型 - 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收
- 区别一:WeakSet中
1 | const ws = new WeakSet(); |
9.2.2. 常用方法
- add(value):添加某个元素,返回WeakSet对象本身
- delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型
- has(value):判断WeakSet中是否存在某个元素,返回boolean类型
- WeakSet不能遍历
- 因为WeakSet只是对对象的弱引用,如果遍历获取到其中的元素,那么有可能造成对象不能正常的销毁
- 所以存储到WeakSet中的对象是没办法获取的
9.2.3. 应用
- 那么这个东西有什么用呢?
- 事实上这个问题并不好回答,使用一个Stack Overflow上的答案
- 实现只能通过new调用类中方法
1 | const pwset = new WeakSet(); |
9.3. Map
9.3.1. 介绍
- 另外一个新增的数据结构是Map,用于存储映射关系
- 可以使用对象来存储映射关系,两者有什么区别呢?
- 事实上对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key)
- 如果希望通过其他类型作为key,比如对象,会自动将对象转成字符串来作为key,就可以使用Map
1 | // 创建方式一 |
9.3.2. 常用方法
size:返回Map中元素的个数set(key, value):在Map中添加key、value,并且返回整个Map对象get(key):根据key获取Map中的valuehas(key):判断是否包括某一个key,返回Boolean类型delete(key):根据key删除一个键值对,返回Boolean类型clear():清空所有的元素forEach(callback, [, thisArg]):通过forEach遍历MapMap也可以通过for of进行遍历
1 | // 记录个数 |
9.4. WeakMap
9.4.1. 介绍
和Map类型相似的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的
那么和Map有什么区别呢?
- 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key
- 区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象
1 | const wm = new WeakMap(); |
9.4.2. 常用方法
- set(key, value):在Map中添加key、value,并且返回整个Map对象
- get(key):根据key获取Map中的value
- has(key):判断是否包括某一个key,返回Boolean类型
- delete(key):根据key删除一个键值对,返回Boolean类型
- 注意:WeakMap也是不能遍历的 p因为没有forEach方法,也不支持通过for of的方式进行遍历
9.4.3. 应用
WeakMap有什么作用呢?
vue中的响应式原理
1
2
3
4
5
6
7
8
9const obj = {
name: "why"
}
function objNameFn1(){
console.log("objNameFn1");
}
function objNameFn2(){
console.log("objNameFn2");
}实现:当obj.name值发生改变时,objNameFn1,objNameFn2函数同时被调用
思路:创建一个WeakMap - weakMap,一个Map - objMap
- objMap中存放key为name,value为[objNameFn1,objNameFn2]的数组形式
- weakMap中存放key为obj,value为m的map形式
- 当obj.name值发生改变时,从weakMap.get(obj)中取得对应的Map为target,再通过target.get(name)取得name对应的函数数组,最后通过for循环执行函数
1 | // 1. 创建WeakMap对象 |
10. Proxy
10.1. 监听对象的操作
需求:有一个对象,希望监听这个对象中的属性被设置或获取的过程
可以通过属性描述符中的存储属性描述符来实现
利用了 Object.defineProperty 的存储属性描述符来对属性的操作进行监听
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21const obj = {
uname: "why",
age: 18
}
Object.keys(obj).forEach(key=>{
let value = obj[key];
Object.defineProperty(obj,key,{
set: function(newValue){
console.log(`监听到给 ${key} 设置值`);
value = newValue;
},
get: function(){
console.log(`监听到获取 ${key} 值`);
return value;
}
})
})
console.log(obj.uname);
obj.uname = "test";
但是这样做有什么缺点呢?
首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性
- 在定义某些属性的时候,初衷其实是定义普通的属性,但是强行将它变成了数据属性描述符
- 其次,如果想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty无能为力
- 因此存储数据描述符设计并不是为了去监听一个完整的对象
10.2. Proxy基本使用
在ES6中,新增了一个
Proxy类,用于创建代理- 也就是说,如果希望监听一个对象的相关操作,可以先创建一个代理对象(Proxy对象)
- 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听想要对原对象进行哪些操作
将上面的案例用Proxy来实现
首先,需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler
const p = new Proxy(target, handler)其次,之后的操作都是直接对Proxy的操作,而不是原有的对象,因为需要在handler里面进行侦听
1 | const obj = { |
10.3. set和get捕获器
- 如果想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap)
- set和get分别对应的是函数类型
- set函数有四个参数
- target:目标对象(侦听的对象)
- property:将被设置的属性key
- value:新属性值
- receiver:调用的代理对象
- get函数有三个参数
- target:目标对象(侦听的对象)
- property:被获取的属性key
- receiver:调用的代理对象
- set函数有四个参数
1 | const obj = { |
10.4. 所有捕获器
- handler.getPrototypeOf():Object.getPrototypeOf 方法的捕捉器
- handler.setPrototypeOf():Object.setPrototypeOf 方法的捕捉器
- handler.isExtensible():Object.isExtensible 方法的捕捉器
- handler.preventExtensions():Object.preventExtensions 方法的捕捉器
- handler.getOwnPropertyDescriptor():Object.getOwnPropertyDescriptor 方法的捕捉器
- handler.defineProperty():Object.defineProperty 方法的捕捉器
- handler.ownKeys():Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
- handler.has():in 操作符的捕捉器
- handler.get():属性读取操作的捕捉器
- handler.set():属性设置操作的捕捉器
- handler.deleteProperty():delete 操作符的捕捉器
- handler.apply():函数调用操作的捕捉器
- handler.construct():new 操作符的捕捉器
10.5. construct和apply
- 捕捉器中还有construct和apply是应用于函数对象的
1 | function foo(){ |
11. Reflect
11.1. Reflect作用
Reflect也是ES6新增的一个API,它是一个
对象,字面的意思是反射Reflect有什么用呢?
- 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法
- 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf()
- 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()
如果有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
- 这是因为在早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些API放到了Object上面
- 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适
- 另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的
- 所以在ES6中新增了Reflect,让这些操作都集中到了Reflect对象上
那么Object和Reflect对象之间的API关系,可以参考 MDN文档
11.2. 常见方法
- Reflect中有哪些常见的方法呢?它和Proxy是一一对应的,也是13个
- Reflect.getPrototypeOf(target):类似于 Object.getPrototypeOf()
- Reflect.setPrototypeOf(target, prototype):设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返 回true
- Reflect.isExtensible(target):类似于 Object.isExtensible()
- Reflect.preventExtensions(target):类似于 Object.preventExtensions()。返回一个Boolean
- Reflect.getOwnPropertyDescriptor(target, propertyKey):类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined
- Reflect.defineProperty(target, propertyKey, attributes):和 Object.defineProperty() 类似。如果设置成功就会返回 true
- Reflect.ownKeys(target):返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys(), 但不会受enumerable影响)
- Reflect.has(target, propertyKey):判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同
- Reflect.get(target, propertyKey[, receiver]):获取对象身上某个属性的值,类似于 target[name]
- Reflect.set(target, propertyKey, value[, receiver]):将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true
- Reflect.deleteProperty(target, propertyKey):作为函数的delete操作符,相当于执行 delete target[name]
- Reflect.apply(target, thisArgument, argumentsList):对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似
- Reflect.construct(target, argumentsList[, newTarget]):对构造函数进行 new 操作,相当于执行 new target(…args)
11.3. Reflect的使用
- 将之前Proxy案例中对原对象的操作,都修改为Reflect来操作
1 | const obj = { |
11.4. Receiver的作用
- 发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?
- 当没有receiver时,源对象(obj)setter、getter访问器中的this并不会改变,仍指向obj,而_name属性也并没有进入代理的get捕捉器
- 当有receiver时,可以通过receiver来改变里面的this
- reveiver 指的就是objProxy代理对象,reflect会将receiver传入obj对象,将obj对象中的this指向receiver,故obj对象中的this进一步救指向objProxy
1 | const obj = { |
11.5. Reflect的construct
执行Student函数中的内容,但是创建出来的对象是Teacher对象
1 | function Student(name,age){ |
12. 响应式
12.1. 什么是响应式
先来看一段代码
m有一个初始化的值,有一段代码使用了这个值
在m有一个新的值时,这段代码可以自动重新执行
1
2
3
4
5
6
7let m = 10;
// 自动重新执行代码块
console.log(m);
console.log(m**2);
m = 40;
这样一种可以自动响应数据变量的代码机制,就称之为是响应式的
再来看一下对象的响应式
1
2
3
4
5
6
7
8// 响应式对象
const obj = {
name: "why"
}
// 需要执行的代码块
let newName = obj.name;
console.log(obj.name);
12.2. 响应式函数设计
首先,执行的代码中可能不止一行代码,所以将这些代码放到一个函数中
那么问题就变成了,当数据发生变化时,自动去执行某一个函数
1
2
3
4
5
6
7// 响应式对象
const obj = {
name: "why"
}
// 需要执行的函数
function foo(){}
但是有一个问题:在开发中有很多的函数,如何区分一个函数需要响应式,还是不需要响应式呢?
很明显,下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出相应
bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作
1
2
3
4
5
6
7
8
9
10
11function foo(){
let newName = obj.name;
console.log(obj.name);
}
function bar(){
const result = 20 + 30;
console.log(result);
console.log("Hello World");
}
12.3. 响应式函数的实现
- 但是怎么区分函数需要响应式?
- 封装一个新的函数watchFn
- 凡是传入到watchFn的函数,就是需要响应式的
- 其他默认定义的函数都是不需要响应式的
1 | const reactiveFns = []; |
12.4. 响应式依赖的收集
目前收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题
- 在实际开发中需要监听很多对象的响应式
- 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数
- 不可能在全局维护一大堆的数组来保存这些响应函数
所以要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数
- 相当于替代了原来的简单 reactiveFns 的数组
1 | class Depend{ |
12.5. 监听对象的变化
通过之前方式来监听对象的变量
- 方式一:通过 Object.defineProperty的方式(vue2采用的方式)
- 方式二:通过new Proxy的方式(vue3采用的方式)
先以Proxy的方式来监听
1 | class Depend{ |
12.6. 对象的依赖管理
目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数
- 但是实际开发中会有不同的对象,另外会有不同的属性需要管理
- 如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
通过WeakMap管理响应式的数据依赖

12.7. 对象依赖管理的实现
getDepend函数专门来管理这种依赖关系
1 | class Depend{ |
12.8. 正确的依赖收集
之前收集依赖的地方是在 watchFn 中
- 但是这种收集依赖的方式根本不知道是哪一个key的哪一个depend需要收集依赖
- 只能针对一个单独的depend对象来添加你的依赖对象
那么正确的应该是在哪里收集呢?
- 应该在调用了Proxy的get捕获器时
- 因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖
1 | class Depend{ |
12.9. 对Depend重构
但是这里有两个问题
- 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次
- 问题二:并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为
所以需要对Depend类进行重构
- 解决问题一的方法:不使用数组,而是使用Set
- 解决问题二的方法:添加一个新的方法,用于收集依赖;
1 | class Depend{ |
12.10. 创建响应式对象
- 目前的响应式是针对于obj一个对象的,可以创建出来一个函数,针对所有的对象都可以变成响应式对象
1 | class Depend{ |
12.11. Vue2响应式原理
前面所实现的响应式的代码,其实就是Vue3中的响应式原理
- Vue3主要是通过Proxy来监听数据的变化以及收集相关的依赖
- Vue2中通过Object.defineProerty 的方式来实现对象属性的监听
将reactive函数进行如下的重构
- 在传入对象时,遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改
- 在setter和getter方法中的逻辑和前面的Proxy是一致的
1 | function reactive(obj){ |