JavaScript学习笔记06-ES6

babel,字面量的增强,解构,let/const,函数参数,Symbol,集合【Set,Map,WeakSet,WeakMap】,Proxy,Reflect,响应式原理及实现

1. babel

babeljs.io

让代码向下兼容ES5,ie浏览器

1.1. ES6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person{
constructor(name){
this.name = name;
}
eating(){
return this.name+' eating';
}
}

class Student extends Person{
constructor(name){
super(name);
}
eating(){
console.log(super.eating());
}
}

var stu = new Student();
stu.eating();

1.2. Person

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

function _typeof(obj) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (obj) {
return typeof obj;
}
: function (obj) {
return obj &&
"function" == typeof Symbol &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
}),
_typeof(obj)
);
}

function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}

function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}

function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}

function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}

var Person = /*#__PURE__*/ (
function () {
function Person(name) {
_classCallCheck(this, Person);
this.name = name;
}
_createClass(Person, [
{
key: "eating",
value: function eating() {
return this.name + " eating";
}
}
]);
return Person;
})();

1.3. Student

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf
? Object.setPrototypeOf.bind()
: function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}

function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf.bind()
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}

function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () {})
);
return true;
} catch (e) {
return false;
}
}

function _superPropBase(object, property) {
while (!Object.prototype.hasOwnProperty.call(object, property)) {
object = _getPrototypeOf(object);
if (object === null) break;
}
return object;
}

function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
} else if (call !== void 0) {
throw new TypeError(
"Derived constructors may only return object or undefined"
);
}
return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}

function _get() {
if (typeof Reflect !== "undefined" && Reflect.get) {
_get = Reflect.get.bind();
} else {
_get = function _get(target, property, receiver) {
var base = _superPropBase(target, property);
if (!base) return;
var desc = Object.getOwnPropertyDescriptor(base, property);
if (desc.get) {
return desc.get.call(arguments.length < 3 ? target : receiver);
}
return desc.value;
};
}
return _get.apply(this, arguments);
}

// _inherits(Student, _Person);
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
/**
创建一个对象,其__proto__ 指向 Person.prototype
Student.prototype指向这个对象
*/
subClass.prototype = Object.create(
superClass && superClass.prototype,
{
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
Object.defineProperty(subClass, "prototype", { writable: false });
// student.__proto__ = Person
// 为了调用父类的静态方法
if (superClass) _setPrototypeOf(subClass, superClass);
}

// var _super = _createSuper(Student);
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();

return function _createSuperInternal() {
// Person
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
/**
Super: Person
arguments: [name]
NewTarget: Student
通过super创建出一个实例,实例原型consrtuctor指向NewTarget
通过Person创建出来一个实例,实例原型consrtuctor指向Student
*/
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}

var Student = /*#__PURE__*/ (
function (_Person) {
_inherits(Student, _Person);

var _super = _createSuper(Student);

function Student(name) {
_classCallCheck(this, Student);
return _super.call(this, name);
}

_createClass(Student, [
{
key: "eating",
value: function eating() {
console.log(
_get(_getPrototypeOf(Student.prototype),
"eating", this).call(this)
);
}
}
]);
return Student;
})(Person);

var stu = new Student();
stu.eating();

2. 字面量的增强

  • ES6中对对象字面量进行了增强,称之为 Enhanced object literals(增强对象字面量)
  • 字面量的增强主要包括下面几部分
    • 属性的简写:Property Shorthand
    • 方法的简写:Method Shorthand
    • 计算属性名:Computed Property Names
1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'why';
var obj = {
name: name,
sayHello: function(){}
}
obj[name+'123']='hahaha';

// 字面量的增强
var obj1 = {
name,
sayHello(){},
[name+'123']:'hahaha'
}

3. 解构Destructuring

  • ES6中新增了一个从数组或对象中方便获取数据的方法,称之为解构Destructuring

  • 可以划分为:数组的解构和对象的解构

3.1. 数组的解构

3.1.1. 基本解构过程

1
2
3
4
5
var names=["abc","cba","nba"];

var [item1,item2,item3]=names;
// abc cba nba
console.log(item1,item2,item3);

3.1.2. 顺序解构

1
2
3
4
var names=["abc","cba","nba"];
var [,,itemz] = names;
// nba
console.log(itemz);

3.1.3. 解构出数组

1
2
3
4
var names=["abc","cba","nba"];
var [item1, ...item] = names;
// [ 'cba', 'nba' ]
console.log(item);

3.1.4. 默认值

1
2
3
4
var names=["abc","cba","nba"];
var [a,b,c,d="default"]= names;
// abc cba nba default
console.log(a,b,c,d);

3.2. 对象的解构

3.2.1. 基本解构过程

1
2
3
4
5
6
7
8
var obj = {
uname: 'why',
age: 18
}

var {uname,age}=obj;
// why 18
console.log(uname,age);

3.2.2. 任意顺序

1
2
3
4
5
6
7
8
var obj = {
uname: 'why',
age: 18
}

var {age,uname}=obj;
// why 18
console.log(uname,age);

3.2.3. 重命名

1
2
3
4
5
6
7
8
var obj = {
uname: 'why',
age: 18
}

var {uname:newName}=obj;
// why
console.log(newName);

3.2.4. 默认值

1
2
3
4
5
6
7
8
var obj = {
uname: 'why',
age: 18
}

var {age,uname:newName,height=180}=obj;
// why 18 180
console.log(newName,age,height);

3.3. 解构的应用场景

  • 比如在开发中拿到一个变量时,自动对其进行解构使用;
  • 比如对函数的参数进行解构;
1
2
3
4
5
6
7
8
9
10
var obj = {
uname: 'why',
age: 18
}

function bar({uname,age}){
console.log(uname,age);
}
// why 18
bar(obj);

4. let/const

4.1. var/let/const

  • 在ES5中声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const

  • let关键字:从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量

  • const关键字

    • const关键字是constant的单词的缩写,表示常量、衡量的意思

    • 它表示保存的数据一旦被赋值,就不能被修改

      1
      2
      3
      const name = "Matt";
      // TypeError: Assignment to constant variable.
      name = "Hello";
    • 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容

      1
      2
      3
      4
      5
      const 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
    3
    var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 全局作用域
var name = "why";

// bar函数作用域
function bar(){
// foo函数作用域
function foo(){
var message = "hi";
console.log(name,message);
}
return foo;
}
// why hi
bar()();
// ReferenceError: message is not defined
// console.log(message);

{
var foo = "foo";
}
// 可以访问到foo变量
console.log(foo);

4.4.2. let/const块级作用域

  • 在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的
  • 但是函数拥有块级作用域,外面依然是可以访问的,这是因为引擎会对函数的声明进行特殊的处理,允许像var那样进行提升
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
var foo = "foo";

let uname = "why";
function bar(){
console.log("bar");
}
class Person{}
}
// foo
console.log(foo);
// ReferenceError: uname is not defined
console.log(uname);
// bar
bar();
// ReferenceError: Person is not defined
new Person();

4.4.3. 嵌套使用

  • js引擎会记录用于变量声明的标识符及其所在的块作用域
  • 只要同一个块中没有重复声明就不会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var name = "Matt";
// Matt
console.log(name);

if(true){
var name = "Hello";
// Hello
console.log(name);
}

let age = 30;
// 30
console.log(age);
if(true){
let age = 26;
// 26
console.log(age);
}

4.5. 块级作用域的应用

4.5.1. if语句代码

1
2
3
4
5
6
7
8
if(true){
var foo = "foo";
let bar = "bar";
}
// foo
console.log(foo);
// ReferenceError: bar is not defined
console.log(bar);

4.5.2. switch语句代码

1
2
3
4
5
6
7
8
9
var color = "red";
switch(color){
case "red":
var foo = "foo";
let bar = "bar";
}
console.log(foo);
// ReferenceError: bar is not defined
console.log(bar);

4.5.3. for语句代码

  • var声明的迭代变量会渗透到循环外部
    • 在之后执行超时逻辑时,所有的j都是同一个变量,输出的都是同一个最终值
  • let声明的迭代变量的作用域仅限于for循环块内部
1
2
3
4
5
6
7
8
for (var j = 0; j<10; j++) {
}
// 10
console.log(j);
for (let i = 0; i < 10; i++) {
}
// ReferenceError: i is not defined
console.log(i);

4.5.4. 按钮点击事件

  • 按钮点击事件function的上层作用域是全局
  • 当全局查找i时,i的值已经是btns.length,所以无论点击哪个按钮,控制台输出的信息不会改变
1
2
3
4
5
6
7
8
const btns = document.querySelectorAll("button");

for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function(){
console.log("按钮"+(i+1)+"被点击");
}
}
console.log(i);
  • 改用立即执行函数
  • 此时点击事件函数的上层作用域是function(n),n的值因为传入的参数值不同而不同
1
2
3
4
5
6
7
8
9
const btns = document.querySelectorAll("button");

for (var i = 0; i < btns.length; i++) {
(function(n){
btns[i].onclick = function(){
console.log("按钮"+n+"被点击");
}
})(i+1);
}
  • 等价于使用let,现在有了块级作用域
  • 当点击事件函数查找上层作用域时,找到的是块级作用域for语句,其中的j值循环执行而不同
1
2
3
4
5
6
const btns = document.querySelectorAll("button");
for (let j = 0; j < btns.length; j++) {
btns[j].onclick = function(){
console.log("按钮"+(j+1)+"被点击");
}
}

4.6. 暂时性死区

  • 它表达的意思是在一个代码中,使用let、const声明的变量,在声明之前,变量都是不可以访问的
  • 这种现象称之为 temporal dead zone(暂时性死区,TDZ)
1
2
3
4
5
6
7
8
9
10
11
12
var foo = "foo";

if(true){
// ReferenceError: Cannot access 'foo' before initialization
// console.log(foo);

let foo = "why";
// why
console.log(foo);
}
// foo
console.log(foo);
  • 在解析代码时,js引擎注意出现到块中的let声明变量foo,在声明之前不能以任何方式来引用未声明的变量
  • 在此阶段引用任何后面才声明的变量会抛出ReferenceError

4.7. 不能混用

  • var和let并不是不同类型的变量,只是指出变量在相关作用域如何存在
1
2
3
var name;
// SyntaxError: Identifier 'name' has already been declared
let 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
    13
    const 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
    12
    function 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
2
3
4
5
6
7
8
9
function foo(){
var x = arguments.length > 0 && arguments[0] != undefined ? arguments[0] : 20;
var y = arguments.length > 0 && arguments[1] != undefined ? arguments[1] : 30;
console.log(x,y);
}

function fun(x=20,y=30){
console.log(x,y);
}

6.1.2. 默认值与解构

1
2
3
4
5
6
7
8
9
// 写法一
function foo1({uname,age}={uname:"why",age:18}){
console.log(uname,age);
}

// 写法二
function foo2({uname="why",age=18}={}){
console.log(uname,age);
}

6.1.3. 默认值顺序

  • 参数的默认值通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的)
  • 但是JavaScript允许不将其放到最后,但是意味着还是会按照顺序来匹配
  • 另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo(x=20,y,z){
console.log(x,y,z);
console.log(foo.length);
}
// 1 2 undefined
// 0
foo(1,2);
// 有默认值的形参最好放到最后
// 20 1 2
foo(undefined,1,2);

// 默认值以及后面的参数都不计算在length之内
function foo1(x,y,z=20){
console.log(x,y,z);
console.log(foo1.length);
}
// 1 2 3
// 2
foo1(1,2,3);

6.2. 剩余参数

6.2.1. 介绍

  • ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中

    • 如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
  • 那么剩余参数和arguments有什么区别呢?

    • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参
    • arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作
    • arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供 并且希望以此来替代arguments的
  • 剩余参数必须放到最后一个位置,否则会报错

1
2
3
4
5
function foo(m,n,...args){
console.log(m,n,args);
}
// 1 2 [ 3, 4 ]
foo(1,2,3,4,5);

6.2.2. 展开语法

  • 展开语法(Spread syntax)

    • 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开
    • 还可以在构造字面量对象时, 将对象表达式按key-value的方式展开
  • 展开语法的场景

    • 在函数调用时使用
    • 在数组构造时使用
    • 在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 函数调用时使用
function foo(x,y,z){
console.log(x,y,z);
}

let names = ["abc","cba","nba"];
// abc cba nba
// 等价于 foo.apply(null,names);
foo(...names);

// 展开字符串
const uname = "why";
// w h y
foo(...uname);

// 数组构造中使用
let newNames = [...names];
// [ 'abc', 'cba', 'nba' ]
console.log(newNames);

// 构建字面量
// { '0': 'abc', '1': 'cba', '2': 'nba', address: '北京市' }
const obj = {...names,address:"北京市"}

6.2.3. 展开运算符是一种浅拷贝

obj中friends属性存储的是原来的地址值,指向原来的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const info = {
uname: "why",
age: 18,
friends: {
uname: "abc"
}
}
const obj = {...info,address:"北京市"}
// { uname: 'why', age: 18, friends: { uname: 'abc' }, address: '北京市' }
console.log(obj);

info.friends.uname = "code";
// { uname: 'why', age: 18, friends: { uname: 'code' }, address: '北京市' }
console.log(obj);

7. 数值的表示

1
2
3
4
5
6
7
8
9
const a = 100;
// 二进制
const b = 0b100;
// 八进制
const c = 0o100;
// 十六进制
const d = 0x100;
// 100 4 64 256
console.log(a,b,c,d);

在ES2021新增特性:数字过长时,可以使用_作为连接符

1
2
const e = 100_000_000;
console.log(e);

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
2
3
4
5
6
7
8
9
const s1 = Symbol();
const s2 = Symbol();
//false
console.log(s1===s2);

const s3 = Symbol("abc");
// 描述
// abc
console.log(s3.description);

8.2. Symbol作为属性名

使用Symbol在对象中表示唯一的属性名

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
27
28
29
const s1 = Symbol();
const s2 = Symbol();
const s3 = Symbol("abc");

// 直接定义字面量
const obj = {
[s1]: "abc",
[s2]: "nba"
}

// 添加属性:属性名赋值
obj[s3]="cba";

// 添加属性:Object.defineProperty
Object.defineProperty(obj,s1,{
enumerable: true,
configurable: true,
writable: true,
value: "aaa"
})

// 获取值
const symbolKeys = Object.getOwnPropertySymbols(obj);
// aaa
// nba
// cba
for (const key of symbolKeys) {
console.log(obj[key]);
}

8.3. 相同值的Symbol

  • 如果想创建相同的Symbol应该怎么来做呢?

    • 使用Symbol.for方法。用参数作为键,在全局符号注册表中创建并重用符号
    • 并且通过Symbol.keyFor方法来获取对应的key
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const 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
    3
    let 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
2
3
4
5
6
7
8
9
10
11
12
// 创建set对象
const s1 = new Set();
s1.add(10);
s1.add(10);
s1.add(14);
// Set(2) { 10, 14 }
console.log(s1);


const s2 = new Set([10,10,14]);
// Set(2) { 10, 14 }
console.log(s2);

去重中的对象问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const s1 = new Set();
s1.add(10);
s1.add(14);
// 去重
s1.add({});
s1.add({});
// Set(4) { 10, 14, {}, {} }
// 此时的{}分指向不同的对象地址
console.log(s1);

const s2 = new Set([10,10,14]);
const obj = {}
s2.add(obj);
s2.add(obj);
// Set(3) { 10, 14, {} }
// 此时的{}属于的是同一个地址值
console.log(s2);

数组中元素去重后放入新数组

1
2
3
4
5
6
7
const arr = [10,11,10,12];
const s3 = new Set(arr);
// 转为数组
const data1 = [...s3];
const data2 = Array.from(s3);
// [ 10, 11, 12 ] [ 10, 11, 12 ]
console.log(data1,data2);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 3
console.log(s3.size);
s3.add(15);
// Set(4) { 10, 11, 12, 15 }
console.log(s3);

s3.delete(12);
// Set(3) { 10, 11, 15 }
console.log(s3);

// true
console.log(s3.has(11));

s3.clear();
// Set(0) {}
console.log(s3);

// 遍历方法一
s2.forEach(item=>console.log(item));

// 遍历方法二
for (const item of s2) {
console.log(item);
}

9.2. WeakSet

9.2.1. 介绍

  • 和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构
  • 那么和Set有什么区别呢?
    • 区别一:WeakSet中只能存放对象类型,不能存放基本数据类型
    • 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收
1
2
3
4
const ws = new WeakSet();

// TypeError: Invalid value used in weak set
// ws.add(100);

9.2.2. 常用方法

  • add(value):添加某个元素,返回WeakSet对象本身
  • delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型
  • has(value):判断WeakSet中是否存在某个元素,返回boolean类型
  • WeakSet不能遍历
    • 因为WeakSet只是对对象的弱引用,如果遍历获取到其中的元素,那么有可能造成对象不能正常的销毁
    • 所以存储到WeakSet中的对象是没办法获取的

9.2.3. 应用

  • 那么这个东西有什么用呢?
    • 事实上这个问题并不好回答,使用一个Stack Overflow上的答案
    • 实现只能通过new调用类中方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const pwset = new WeakSet();
class Person {
constructor(){
pwset.add(this);
}

running(){
console.log(this);
if(!pwset.has(this)){
throw new Error("不能通过其他方法调用running方法");
}
console.log("running");
}
}

const p = new Person();
// Person {}
// running
p.running();

// { uname: 'why' }
// Error: 不能通过其他方法调用running方法
p.running.call({uname:"why"});

9.3. Map

9.3.1. 介绍

  • 另外一个新增的数据结构是Map,用于存储映射关系
  • 可以使用对象来存储映射关系,两者有什么区别呢?
    • 事实上对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key)
    • 如果希望通过其他类型作为key,比如对象,会自动将对象转成字符串来作为key,就可以使用Map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建方式一
const m = new Map();
m.set(obj1,"abc");
m.set(obj2,"nba");
// 遍历
for (const key of m.keys()) {
console.log(m.get(key));
}

// 创建方式二
const m1 = new Map([
[obj1,"abc"],
[obj2,"nba"]
]);
for (const key of m1.keys()) {
console.log(m1.get(key));
}

9.3.2. 常用方法

  • size:返回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进行遍历

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 记录个数
// 2
console.log(m.size);

// 添加key,value
m.set("abc","123");
/**
* Map(3) {
{ uname: 'why' } => 'abc',
{ uname: 'test' } => 'nba',
'abc' => '123'
}
*/
console.log(m);

// 得到key对应value值
// 123
console.log(m.get("abc"));

// 删除key
m.delete(obj1);
/**
* Map(2) { { uname: 'test' } => 'nba', 'abc' => '123' }
*/
console.log(m);

// 是否存在该key
// true
console.log(m.has("abc"));

// 遍历value值
/**
* nba
* 123
*/
m.forEach(item=>{
console.log(item);
})

// 得到的是每一个元素的key和value
for (const item of m) {
console.log(item);
}

9.4. WeakMap

9.4.1. 介绍

  • 和Map类型相似的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的

  • 那么和Map有什么区别呢?

    • 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key
    • 区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象
1
2
3
4
5
6
const wm = new WeakMap();

wm.set({},"123");

// TypeError: Invalid value used as weak map key
wm.set("abc","123");

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
      9
      const 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
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 创建WeakMap对象
const weakMap = new WeakMap();
const objMap = new Map();

// 2. 对obj.name 进行数据收集
objMap.set("name",[objNameFn1,objNameFn2]);
weakMap.set(obj,objMap);

// 3. 值发生改变时执行函数
obj.name="test";
const target = weakMap.get(obj);
const fns = target.get("name");
fns.forEach(item => item());

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
      21
      const 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
2
3
4
5
6
7
const obj = {
uname: "why",
age: 18
}


const proxy = new Proxy(obj,{});

10.3. set和get捕获器

  • 如果想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap)
  • set和get分别对应的是函数类型
    • set函数有四个参数
      • target:目标对象(侦听的对象)
      • property:将被设置的属性key
      • value:新属性值
      • receiver:调用的代理对象
    • get函数有三个参数
      • target:目标对象(侦听的对象)
      • property:被获取的属性key
      • receiver:调用的代理对象
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
27
28
29
30
31
32
33
34
35
36
37
38
const obj = {
uname: "why",
age: 18
}


const proxy = new Proxy(obj,{
set: function(target,key,value){
console.log(`set捕捉器监听到${key}属性值从${target[key]}改为${value}`,target);
target[key]=value;
},
get: function(target,key){
console.log(`get捕捉器监听到${key}属性被访问了`,target);
return target[key];
},
has: function(target,key){
console.log(`has捕捉器监听到查询${key}属性,为${key in target}`);
return key in target;
},
deleteProperty: function(target, key){
console.log(`deleteProperty捕捉器监听到删除${key}属性`);
delete target[key];
}
});

// why
console.log(proxy.uname);

proxy.uname = "test";
// test
console.log(proxy.uname);
// test
console.log(obj.uname);
// false
console.log("name" in proxy);

delete proxy.age;
console.log(obj);

10.4. 所有捕获器

  1. handler.getPrototypeOf():Object.getPrototypeOf 方法的捕捉器
  2. handler.setPrototypeOf():Object.setPrototypeOf 方法的捕捉器
  3. handler.isExtensible():Object.isExtensible 方法的捕捉器
  4. handler.preventExtensions():Object.preventExtensions 方法的捕捉器
  5. handler.getOwnPropertyDescriptor():Object.getOwnPropertyDescriptor 方法的捕捉器
  6. handler.defineProperty():Object.defineProperty 方法的捕捉器
  7. handler.ownKeys():Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
  8. handler.has():in 操作符的捕捉器
  9. handler.get():属性读取操作的捕捉器
  10. handler.set():属性设置操作的捕捉器
  11. handler.deleteProperty():delete 操作符的捕捉器
  12. handler.apply():函数调用操作的捕捉器
  13. handler.construct():new 操作符的捕捉器

10.5. construct和apply

  • 捕捉器中还有construct和apply是应用于函数对象的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(){
console.log("foo函数被调用",this,arguments);
return "foo";
}

const fooProxy = new Proxy(foo,{
apply: function(target, thisArg, otherArg){
console.log("函数的apply监听");
return target.apply(thisArg,otherArg);
},
construct: function(target, argArray, newTarget){
console.log(target, argArray, newTarget);
return new target();
}
});

// [Function: foo] [] [Function: foo]
// foo函数被调用 foo {} [Arguments] {}
const f = new fooProxy();

// 函数的apply监听
// foo函数被调用 {} [Arguments] { '0': 'abc', '1': 'nba' }
fooProxy.apply({},["abc","nba"]);

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个
  1. Reflect.getPrototypeOf(target):类似于 Object.getPrototypeOf()
  2. Reflect.setPrototypeOf(target, prototype):设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返 回true
  3. Reflect.isExtensible(target):类似于 Object.isExtensible()
  4. Reflect.preventExtensions(target):类似于 Object.preventExtensions()。返回一个Boolean
  5. Reflect.getOwnPropertyDescriptor(target, propertyKey):类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined
  6. Reflect.defineProperty(target, propertyKey, attributes):和 Object.defineProperty() 类似。如果设置成功就会返回 true
  7. Reflect.ownKeys(target):返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys(), 但不会受enumerable影响)
  8. Reflect.has(target, propertyKey):判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同
  9. Reflect.get(target, propertyKey[, receiver]):获取对象身上某个属性的值,类似于 target[name]
  10. Reflect.set(target, propertyKey, value[, receiver]):将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true
  11. Reflect.deleteProperty(target, propertyKey):作为函数的delete操作符,相当于执行 delete target[name]
  12. Reflect.apply(target, thisArgument, argumentsList):对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似
  13. Reflect.construct(target, argumentsList[, newTarget]):对构造函数进行 new 操作,相当于执行 new target(…args)

11.3. Reflect的使用

  • 将之前Proxy案例中对原对象的操作,都修改为Reflect来操作
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
const obj = {
uname: "why",
age: 18
}

const objProxy = new Proxy(obj,{
set: function(target,key,newValue){
console.log("set 捕捉器");
const result = Reflect.set(target,key,newValue);
if(result){
console.log("设置成功");
}
else{
console.log("设置失败");
}
},
get: function(target,key){
console.log("get 捕捉器");
return Reflect.get(target,key);
}
});

console.log(objProxy.uname);

objProxy.uname = "test";

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
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
27
28
29
30
31
const obj = {
_name: "why",
set name(newValue){
this._name = newValue;
},
get name(){
return this._name;
}
}

// obj.name = "test"
// console.log(obj.name);

const objProxy = new Proxy(obj,{
set: function(target,key,newValue,receiver){
console.log(`set ${key} ${newValue}`);
Reflect.set(target,key,newValue,receiver);
},
get: function(target, key, receiver){
console.log(`get ${key}`, receiver);
return Reflect.get(target,key,receiver);
}
})

// set name test
// set _name test
objProxy.name = "test";
// get name { _name: 'test', name: [Getter/Setter] }
// get _name { _name: 'test', name: [Getter/Setter] }
// test
console.log(objProxy.name);

11.5. Reflect的construct

执行Student函数中的内容,但是创建出来的对象是Teacher对象

1
2
3
4
5
6
7
8
9
10
11
function Student(name,age){
this.name = name;
this.age = age;
}
function Person(){}

const stu = Reflect.construct(Student,["why",18],Person);
// Person { name: 'why', age: 18 }
console.log(stu);
// true
console.log(stu.__proto__===Person.prototype);

12. 响应式

12.1. 什么是响应式

  • 先来看一段代码

    • m有一个初始化的值,有一段代码使用了这个值

    • 在m有一个新的值时,这段代码可以自动重新执行

      1
      2
      3
      4
      5
      6
      7
      let 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
      11
      function 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const reactiveFns = [];

function watchFn(fn){
reactiveFns.push(fn);
}

watchFn(function(){
let newName = obj.name;
console.log(obj.name);
})

watchFn(function(){
console.log("my name is " + obj.name);
})

const obj = {
name: "why"
}

obj.name = "test";
reactiveFns.forEach( fn => fn());

12.4. 响应式依赖的收集

  • 目前收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题

    • 在实际开发中需要监听很多对象的响应式
    • 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数
    • 不可能在全局维护一大堆的数组来保存这些响应函数
  • 所以要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数

    • 相当于替代了原来的简单 reactiveFns 的数组
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
27
28
29
class Depend{
constructor(){
this.reactiveFns = [];
}
addDepend(fn){
this.reactiveFns.push(fn);
}
notify(){
this.reactiveFns.forEach(fn=>{
fn();
})
}
}

const dep = new Depend();

dep.addDepend(function(){
let newName = obj.name;
console.log(obj.name);
})
dep.addDepend(function(){
console.log("my name is " + obj.name);
})

const obj = {
name: "why"
}
obj.name="test";
dep.notify();

12.5. 监听对象的变化

  • 通过之前方式来监听对象的变量

    • 方式一:通过 Object.defineProperty的方式(vue2采用的方式)
    • 方式二:通过new Proxy的方式(vue3采用的方式)
  • 先以Proxy的方式来监听

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Depend{
constructor(){
this.reactiveFns = [];
}
addDepend(fn){
this.reactiveFns.push(fn);
}
notify(){
this.reactiveFns.forEach(fn=>{
fn();
})
}
}

const obj = {
name: "why"
}

const dep = new Depend();

dep.addDepend(function(){
let newName = obj.name;
console.log(obj.name);
})
dep.addDepend(function(){
console.log("my name is " + obj.name);
})

const objProxy = new Proxy(obj,{
set: function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
dep.notify();
},
get: function(target,key,receiver){
return Reflect.get(target,key,receiver);
}
})

// test
// my name is test
objProxy.name="test";

12.6. 对象的依赖管理

  • 目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数

    • 但是实际开发中会有不同的对象,另外会有不同的属性需要管理
    • 如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
  • 通过WeakMap管理响应式的数据依赖

12.7. 对象依赖管理的实现

getDepend函数专门来管理这种依赖关系

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
27
28
29
30
class Depend{
constructor(){
this.reactiveFns = [];
}
addDepend(fn){
this.reactiveFns.push(fn);
}
notify(){
this.reactiveFns.forEach(fn=>{
fn();
})
}
}

const targetMap = new WeakMap();
function getDepends(obj,key){
// 根据对象获取对应的map对象
let objMap = targetMap.get(obj);
if(!objMap){
objMap = new Map();
targetMap.set(obj,objMap);
}
// 根据key获取Depend对象
let depend = objMap.get(key);
if(!depend){
depend = new Depend();
objMap.set(key,depend);
}
return depend;
}

12.8. 正确的依赖收集

  • 之前收集依赖的地方是在 watchFn 中

    • 但是这种收集依赖的方式根本不知道是哪一个key的哪一个depend需要收集依赖
    • 只能针对一个单独的depend对象来添加你的依赖对象
  • 那么正确的应该是在哪里收集呢?

    • 应该在调用了Proxy的get捕获器时
    • 因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Depend{
constructor(){
this.reactiveFns = [];
}
addDepend(fn){
this.reactiveFns.push(fn);
}
notify(){
this.reactiveFns.forEach(fn=>{
fn();
})
}
}

const targetMap = new WeakMap();
function getDepends(obj,key){
// 根据对象获取对应的map对象
let objMap = targetMap.get(obj);
if(!objMap){
objMap = new Map();
targetMap.set(obj,objMap);
}
// 根据key获取Depend对象
let depend = objMap.get(key);
if(!depend){
depend = new Depend();
objMap.set(key,depend);
}
return depend;
}

const obj = {
name: "why",
age: 18
}

let reaceiveFn = null;
function watchFn(fn){
reaceiveFn = fn;
fn();
reaceiveFn = null;
}


const objProxy = new Proxy(obj,{
set: function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
const dep = getDepends(obj,key);
dep.notify();
},
get: function(target,key,receiver){
// 根据target,key获取对应的depend
const dep = getDepends(obj,key);
// 给depend对象中添加响应函数
dep.addDepend(reaceiveFn);
return Reflect.get(target,key,receiver);
}
})


watchFn(function(){
console.log("my age is " + objProxy.age);
})
watchFn(function(){
console.log("my name is " + objProxy.name);
})

objProxy.name="test";

12.9. 对Depend重构

  • 但是这里有两个问题

    • 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次
    • 问题二:并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为
  • 所以需要对Depend类进行重构

    • 解决问题一的方法:不使用数组,而是使用Set
    • 解决问题二的方法:添加一个新的方法,用于收集依赖;
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
27
28
29
30
31
32
33
class Depend{
constructor(){
this.reactiveFns = new Set();
}
addDepend(fn){
this.reactiveFns.add(fn);
}
depend(){
if(reaceiveFn){
this.reactiveFns.add(reaceiveFn);
}
}
notify(){
this.reactiveFns.forEach(fn=>{
fn();
})
}
}

const objProxy = new Proxy(obj,{
set: function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
const dep = getDepends(obj,key);
dep.notify();
},
get: function(target,key,receiver){
// 根据target,key获取对应的depend
const dep = getDepends(obj,key);
// 给depend对象中添加响应函数
dep.depend();
return Reflect.get(target,key,receiver);
}
})

12.10. 创建响应式对象

  • 目前的响应式是针对于obj一个对象的,可以创建出来一个函数,针对所有的对象都可以变成响应式对象
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Depend{
constructor(){
this.reactiveFns = new Set();
}
addDepend(fn){
this.reactiveFns.add(fn);
}
depend(){
if(reaceiveFn){
this.reactiveFns.add(reaceiveFn);
}
}
notify(){
this.reactiveFns.forEach(fn=>{
fn();
})
}
}

const targetMap = new WeakMap();
function getDepends(obj,key){
// 根据对象获取对应的map对象
let objMap = targetMap.get(obj);
if(!objMap){
objMap = new Map();
targetMap.set(obj,objMap);
}
// 根据key获取Depend对象
let depend = objMap.get(key);
if(!depend){
depend = new Depend();
objMap.set(key,depend);
}
return depend;
}

let reaceiveFn = null;
function watchFn(fn){
reaceiveFn = fn;
fn();
reaceiveFn = null;
}

function reactive(obj){
return new Proxy(obj,{
set: function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
const dep = getDepends(obj,key);
dep.notify();
},
get: function(target,key,receiver){
// 根据target,key获取对应的depend
const dep = getDepends(obj,key);
// 给depend对象中添加响应函数
dep.depend();
return Reflect.get(target,key,receiver);
}
})
}

const objReactive = reactive({
address: "北京市"
})
watchFn(function(){
console.log("my address is " + objReactive.address);
})

objReactive.address = "杭州市";

12.11. Vue2响应式原理

  • 前面所实现的响应式的代码,其实就是Vue3中的响应式原理

    • Vue3主要是通过Proxy来监听数据的变化以及收集相关的依赖
    • Vue2中通过Object.defineProerty 的方式来实现对象属性的监听
  • 将reactive函数进行如下的重构

    • 在传入对象时,遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改
    • 在setter和getter方法中的逻辑和前面的Proxy是一致的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 function reactive(obj){
Object.keys(obj).forEach(key=>{
let value = obj[key];
Object.defineProperty(obj,key,{
get: function(){
const dep = getDepends(obj,key);
dep.depend();
return value;
},
set: function(newValue){
value = newValue;
const dep = getDepends(obj,key);
dep.notify();
}
})
})
return obj;
}
本文结束  感谢您的阅读
  • 本文作者: Wang Ting
  • 本文链接: /zh-CN/2019/08/28/JavaScript学习笔记-ES6/
  • 发布时间: 2019-08-28 08:41
  • 更新时间: 2024-03-15 23:05
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!