JS高级部分详解
1.浏览器运行原理
JavaScript
是一门高级语言,它主要运行在浏览器上,其表达形式更接近人类的思维,但是机器并不能直接识别高级语言,所以需要被转换为机器指令。所以我们以下就探讨下JS代码是如何在浏览器中被执行的。
在浏览器中输入url后会发生什么
我们在讨论浏览器之前先思考下我们经常使用的浏览器在输入打开一个网址后会发生什么。
- 首先浏览器会对输入的
ur
l进行编码,保证一些特殊字符转义不会发生歧义,详细的编码规则可以自行研究。 - 进行
dns
解析查询ip
。 - 进行
tcp
连接的的三次握手。 - 建立连接,请求获取
html
文件,这里如果有缓存的话就需要考虑缓存相关情况。 获取
html
文件后就会进行html
解析,解析html
文档生成dom
树,加载样式文件生成cssom
树,如果遇到js
就会去执行js
文件。然后根据最终生成的dom
树和cssom
树生成渲染树,按照顺序进行布局排版将每一个节点布局在屏幕的正确位置上,最后遍历渲染绘制所有节点,为节点适用对应的样式。
浏览器内核
不同的浏览器有着不同的内核,以下是一些常用浏览器使用的内核:
Gecko
:早期被Netscape
和Mozilla Firefox
浏览器浏览器使用Trident
:微软开发,被IE4~IE11浏览器使用,但是Edge
浏览器已经转向Blink
Webkit
:苹果基于KHTML开发、开源的,用于Safari
,Google Chrome
之前也在使用Blink
:是Webkit
的一个分支,Google开发,目前应用于Google Chrome
、Edge
、Opera
等
浏览器内核通常指的是浏览器的排版引擎。
js引擎
高级的编程语言都是需要转成最终的机器指令来执行的,我们编写的js
最终都会交由CPU
执行,js
引擎的作用就是将js
代码翻译成cpu
能够执行的机器代码。
常见的js
引擎:
SpiderMonkey
:第一款JavaScript
引擎,由Brendan Eich
开发(也就是JavaScript
作者)。Chakra
:微软开发,用于IT
浏览器。JavaScriptCore
:WebKit
中的JavaScript
引擎,Apple
公司开发(小程序中就是使用的这个引擎来执行js
的)。V8
:Google
开发的强大JavaScript
引擎,也帮助Chrome
从众多浏览器中脱颖而出。
这里主要聊一些v8引擎:
v8基本的认识可以件这篇文章深入理解Node.js (一)---Node基础概念
官方的解析图:
- 首先代码经过
blink
交由stream
进行编码的处理转换。 scanner
会进行词法分析,词法分析会将代码转为tokens
tokens
会进行解析和预解析,因为并不知所有的js
在一开始都要执行,所以对所有的代码进行解析就会有一定的额效率问题,所以v8
引擎就实现了lazy parsing
(延迟加载)的方案,主要作用是将不必要的函数进行预解析,对于函数的全量解析则是在函数需要执行的时候再进行。token
经过解析后会生成ast
树,然后再交由v8
引擎中的几个重要的模块进行处理
2.js执行的过程
首先讲解下一些概念名词
全局对象
js
引擎在执行代码之前会在堆内存中创建一个全局对象,Golbal Object
(GO)
- 该对象所有作用域(
scope
)都可以访问。 - 该对象中包括了很多的内置对象(
Date
、Array
、String
、Number
、setTimeout
、setInterval
等)。 - 对象内部有个
window
属性指向自己。
执行上下文栈
js
引擎内部有一个执行上下文栈(Execution Context Stack
,简称ECS
),它是用于执行代码的调用栈。
执行上下文
在执行上下文栈中执行的是全局的代码块。为了全局代码能够正常的执行,内部还需要构建一个全局执行上下文(Global Execution Context
,简称GEC
),GEC
会被放入到ECS
中执行。然后全局执行上下文中存在一个变量对象(Variable Object
,简称VO
)指向GO
。
GEC
被放入到ECS
里面后包含两个部分:
- 在代码执行前的编译阶段
parse
转成AST
的过程中,会创建一个VO
(指向GO
),将全局的变量,函数等加入到GO
中,此时不会进行赋值操作,这个过程也称之为作用域提升(hoisting
)。 - 对变量进行赋值或者执行函数中的代码。
例如下面一段代码:
上述代码执行流程如下:
代码中有函数怎么执行
在执行代码的过程中遇到了函数,就会根据函数体创建一个函数执行上下文(Function Execution
Context
,简称FEC
),并且添加到ECS
中。
FEC
包含三个部分:
- 在解析成AST树结构的时候会创建一个活动对象(
Activation
Object
,简称AO
),AO
包含形参,argument
,函数定义和指向函数的对象,定义的变量。 - 作用域链:由
VO
(在函数中就是AO
)和父级的VO
构成,查找的时候会就近一层一层的查找。 this
绑定的值。
例如以下代码:
变量环境和记录
在新版的ECMA
标准中,我们将变量对象称之为变量环境(VE
),里面的属性和函数声明称之为环境记录。
简单总结
- 作用域在代码编译的时候就会被确定。
- 函数中的作用域和其调用位置没关系,与定义位置有关。
3.内存管理
分配内存
任何的编程语言再执行的过程中都需要分配内存,当然不同的语言对于内存的管理有所不同,js
是自动管理内存的语言。
当然内存管理大致有以下生命周期:
- 申请分配内存。
- 使用内存,存放对象等。
- 释放内存。
js
在定义变量的时候分配内存,对于不同的数据类型内存分配的方式是不一样的:
- 对于基本类型直接在栈空间进行分配。
- 对于复杂数据类型会在堆空间进行分配,并将对应空间的指针返回值给栈空间中的变量引用。
当然不同的引擎对于内存空间的分配管理可能不同,这里便于理解就大致分为栈空间和堆空间。
垃圾回收
内存是有限的,所以内存不再使用的时候需要对其进行释放。在一些手动管理内存的编程语言中需要通过一些自己的方式来手动释放内存。这样的方式效率低下,而且对于编程者的要求更高,且容易造成内存泄漏(引用的内存空间不需要使用时,但又没有释放掉)。
当然大部分的现代编程语言都有自己的垃圾回收(Garbage
Collection
,简称GC
)机制了。
当然GC
如何知道那些对象不再使用就需要用到GC
算法了。
常见的GC
算法:
引用计数
当一个对象有一个引用指向它的时候,这个对象的引用计数就+1
,当这个对象的引用计数为0的时候就表示没有引用指向它,所以就可以将这个对象销毁掉。
但是这个方式有个弊端就是,当遇到一个循环引用的对象的时候,这两个对象的引用永远都是2,所以永远不会被销毁。
当然解决方式是将两个对象都赋值null
。
标记清除
该算法是设置一个根对象,垃圾回收器会定时从这个根开始,找所有从根开始有引用的对象,对于那些没有引用到的对象,就认为是不可用的对象,加上不可用的标记。例如函数内部声明一个变量,这个变量会被加上存在于上下文中的标记,在上下文中的变量逻辑上永远不应该被释放掉,只要上下文在运行中,我们就能访问到这个变量。所以当变量离开上下文,我们访问不到这个变量了,就会被加上离开上下文的标记。之后内存清理就会清除这些有标记的变量了。
当然标记清除中的标记方式有很多种,改算法是js
引擎中较为常用的一种。
4.闭包
在js
中,函数非常的灵活,作为头等公民,它既可以作为一个函数的参数,也可以作为返回值返回。
接下来探讨下js
闭包的定义。
我最初对闭包的认识是在接触到自执行函数的时候,那时候只知道自执行函数内部形成了闭包可以实现模块化。后来在一些书籍上了解到闭包是一个函数去访问另一个作用域中的变量。但是对于闭包这个概念始终感觉很模糊。
一些权威机构和大佬对闭包的解释:
维基百科
闭包成为词法闭包或函数闭包,是在支持头等函数的编程语言中,实现词法绑定的一种计数。闭包在实现上是一个结构体,它存储了一个函数和另一个关联的环境(相当于一个符号查找表)。闭包和函数最大的区别就是当捕捉闭包的时候,闭包的自由变量会在捕捉的时候被确定,这样即使脱离了捕捉时的上下文,也能照常运行。
MDN
一个函数对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就被称之为闭包,也就是说,闭包可以让你在一个内层函数访问到其外层函数的作用域,在 JavaScript
中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
coderwhy的理解
一个普通的函数,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包。
广义角度来看:js
中的函数都是闭包。
狭义角度来看:js
函数中如果访问了外层作用域的变量,那该函数就是一个闭包。
闭包的访问过程
以下代码就是形成了闭包:
我们首先看下上述出现的函数作用域包含了那些部分:
可以看到返回的函数内部作用域就包含了一个闭包(closure
)。
基本执行过程:
在执行fn1
前
执行fn1fn
上下文出栈,bar
上下文入栈
可以发现bar
函数对于父级作用域有引用关系。所以fn
的内部的活动对象不会被销毁。
如果要销毁可以执行以下代码:
闭包和内存泄漏
上面我们通过简单的画图解释了使用闭包执行的整个过程,这里就得出了一个结论就是使用闭包可能会导致内存泄漏,因为我们可能保留了一些变量的引用导致其继续占用内存。
这里用一个案例来展示下使用闭包时内存的一个大致使用情况。
使用谷歌浏览器的性能测试录制展示如下(20s):
可以看到内存使用情况先循序递增在瞬间下降,这里时间轴只做个大致参考。因为浏览器还有其他的干扰因素导致测试结果不是理想状态。
循序清空数组一半(测试20s):
可以看到大致降低了一半的内存使用情况。
闭包和自由变量
还有一个问题,既然闭包中函数访问了外部作用域的自由变量,那么没有被访问的自由变量会不会被销毁呢?
首先在ECMA
标准中说明的,闭包的函数有对外部函数的活动对象的引用关系,从之前我们写的执行流程图也可以看出,所以理论上外部的自由变量是一直存在的,不管内部是否有没有使用。但是不同的js
引擎对于这个标准的处理有所不同,v8
引擎就只会保留使用到的自由变量,其他未使用的的自由变量都会被销毁掉。
在谷歌浏览器中调试下试试:
可以看到闭包中只有age
这个变量,再在控制台上输出下age
和name
,这里断点打在函数内部的,所以控制台是可以访问函数内部的变量的。
5.this
this
在js
中方便了我们使用一些属性和方法。
this的指向
this在全局环境下
在浏览器,非严格模式下,
this
指向window
。node
环境为{}
,但是在node
环境中的普通函数中的this
是global
对象
当然一般在全局作用于下很少使用this
。
我们再看一下一组代码:
通过上述结果我们可以以得出:
- 函数在调用的时候会给
this
一个默认值。 this
的值和函数的定义位置没有关系。this
的绑定和调用方式以及调用的位置有关系。this
是在运行时被绑定的。
this的绑定规则
默认绑定
函数的独立调用,没有被绑定到某个对象上调用。隐式绑定
函数的调用位置是通过某个对象发起的。隐式绑定有个前提,必须在调用的对象中有一个对函数的引用,如果没有这样的引用汇报错的,正是因为这个引用才将
this
间接的绑定到了这个对象上。显示绑定
通过一些方法,显示的改变this
指的对象。
使用call
,apply
,bind
方法。new 绑定
注意执行new发生了什么:
- 创建一个对象。
- 将新对象的
__proto__
设置为构造函数的prototype
。 - 将构造函数的
this
指向新对象。 执行构造函数中的代码。
- 如果构造函数返回非空对象,则返回该对象,否则返回刚创建的新对象。
this的绑定优先级
显示绑定(call,apply,bind)高于隐式绑定
注意bind
绑定后的函数this
不能被显示改变了,即连续调用bind
或者使用bind
再使用call
或者apply
,其this
永远为第一次使用bind
绑定后的this
:
new 高于显示绑定,不能和apply call bind一起使用
总结:
new
绑定 > 显示绑定(apply/call/bind
) > 隐式绑定(obj.foo()
) > 默认绑定(独立函数调用)。
this的特殊情况
忽略显示绑定:
当执行apply/call/bind
:传入null/undefined
时会自动将this
绑定成全局对象
间接函数引用:
间接函数引用导致丢失this
。
箭头函数与this
箭头函数有着一些特殊规则:
- 箭头函数不能绑定
this
和argument
,不能通过显示绑定方法来给箭头函数绑定this
。 - 箭头函数不能和
new
一起使用(没有显示原型,不能作为构造函数)。 - 箭头函数的
this
为上层作用域终端的this
。
这里写几个案例:
6.函数式编程
使用js模拟call、apply、bind方法
这三个方法都是js
原生自带的方法,用于改变函数内部的this
指向,但是它们原生是使用C++实现的,这里我们为了了解this
使用使用js
来简单模拟下,当然会实现主要功能,但实际部分边界情况不会考虑。
call方法实现
apply方法实现
bind方法实现
arguments的相关认识
这个参数是传递给一个函数的参数类数组对象,注意不是数组,是类数组,本质上是对象,只是能调用length
方法和通过索引访问。
arguments
转为数组
有以下方法:
注意:箭头函数也是没有arguments
这个参数的,所以它的arguments
属于其最近上层作用域普通函数中的。
纯函数
概念
函数式编程中有个很重要的概念就是纯函数。
如果一个函数满足以下条件,就可以称之为纯函数:
- 函数输入相同的值,总是输出相同的值。
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
副作用的理解
再执行一个函数的时候除了返回函数值外,还对调用函数产生了附加影响,比如修改了全局对象,修改参数或者改变了外部内存或者环境。
案例
以下介绍几个纯函数案例:
slice
slice
截取数组时不会对原数组进行任何操作,而是生成一个新的数组。slice
这里就属于纯函数,
splice
splice
执行的时候会对原数组进行修改,所以不是纯函数。
纯函数的优势
- 写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或 者依赖其他的外部变量是否已经发生了修改。
- 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出。
比如react
和vue
中都不建议直接修改props
的值。
柯里化
概念
柯里化用于将一个接收多个参数的函数,变为接收单个参数的函数,并返回另一个函数来处理剩下的参数的函数。
简单来讲柯里化就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数的过程。
基本结构
为什么使用柯里化
在函数式编程中,我们往往希望一个函数的责任尽可能的单一,而不是讲一大堆逻辑交由一个函数进行处理。柯里化就是希望一个函数尽可能完成单一的事情,满足单一职责原则(SRP:single responsibility principle)。
同时使用柯里化还可以进行逻辑复用:
打印日志:
自动柯里化
我们上面都是自己写的柯里化结构函数,这里我们实现一个能将普通函数转为柯里化函数的方法:
组合函数
有时候我们需要对一个参数进行处理,我们可能将处理步骤分为多个函数进行调用处理:
上述就是基本的调用过程,但是看得出,函数嵌套调用导致过程并不清晰易懂。
我们来优化下:
上述的调用过程就很清晰易懂了,但是这个组合函数并不通用,万一我们想传递不固定个数的函数呢,这里封装一个较为通用性的方法:
js的严格模式
在讲解js
的严格模式之前线补充一些额外的知识。
with语句
with语句用于扩展一个语句的作用域链。
JavaScript
在查找一个变量的额时候会通过作用域链来进行查找,作用域链是跟执行代码的上下文或者包含这个变量的函数有关,with
语句会将某个对象添加到作用域链的顶端,如果在with
的代码块中有个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。
所以我们可以通过with
来简化对某一对象取值的多余代码:
当然with
也有很大的弊端,会导致语义不明:
总之with
知道大致用法就行,在实际的代码中并不推荐使用它,它会造成语义不明或者兼容问题。
eval
eval
函数是JavaScript
内置的一个函数,可以传入一个字符串进去,函数会将该字符串作为代码进行解析执行。
反正不建议在实际的开发中使用eval
,它有以下弊端:
- 可读性差。
- 不安全,会执行传入的字符串。
- 性能问题,
eval
的执行必须经过JS解释器,不能被JS引擎优化。
严格模式
严格模式是es5
标准中就提出的,目的是为了显示JavaScript
的灵活,因为JavaScript
的语法非常的宽松,所以严格模式就是为了避免写出居于隐形错误的代码,对代码有更严格的检测和执行。
严格模式的作用:
- 严格模式通过抛出错误来消除一些原有的 静默(
silent
)错误。 - 严格模式让JS引擎在执行代码时可以进行更多的优化,不需要对一些特殊的语法进行优化。
- 严格模式禁用了在未来版本中可能会定义的一些语法,保证了代码的执行兼容性。
开启严格模式
可以根据粒度来进行开启。
全局开启,在js
文件的顶层添加 "use strict";即可
局部开启,对一个函数开启严格模式:
开启后常见的语法限制:
- 无法意外的创建全局变量 。
- 严格模式会使引起静默失败(
silently fail
,注:不报错也没有任何效果)的赋值操作抛出异常 。 - 严格模式下试图删除不可删除的属性 。
- 严格模式不允许函数参数有相同的名称 。
- 不允许0的八进制语法 。
- 在严格模式下,不允许使用
with
。 - 在严格模式下,
eval
不再为上层引用变量 。 - 严格模式下,
this
绑定不会默认转成对象。
举几个例子:
禁止意外创建变量
开启前:
正常打印。
开启后:
不允许函数有相同的参数名
开启前:
开启后:
eval
不再为上层引用变量
开启前:
开启后:
7.js面向对象
JavaScript
支持多种编程范式,这里也支持面向对象编程。
对象在js
中被设计为一组无需的属性集合,像是一个哈希表,由key
和value
组成。key
是一个标识符名称,value
可以是任意类型,也可以是其他对象或者函数类型,当然如果值是一个函数,那么可以称之为对象的方法。
创建对象
构造函数方式
字面量方式
工厂模式
上述方法在创建单个对象的时候可以使用,但是如果我们要批量创建对象的话,上面的两种方式就显得不太方便,我们要为每个对象进行属性的赋值,所以可以通过工厂模式批量进行对象的创建。
构造函数方法
我们通过构造函数的方式能够创建多个对象,但是这有个问题就是,我们创建出来的对象打印出来的类型都为Object
。但实际上我们要求创建的对象要有特定的类型,这里就要提到构造函数。
构造函数被称之为构造器,一般在创建函数就会调用,在js
种构造函数就是一个普通的函数,但是根据其调用方式,有着不同的区别,如果该函数以new
操作符进行调用,那该函数就称之为构造函数。
首先说下new
操作符进行了那些步骤:
- 在内存中创建一个新的对象(空对象)。
- 将创建出来的该对象的的
[[prototype]]
属性赋值为构造函数的[[prototype]]
属性。 - 构造函数内部的
this
,会指向创建出来的新对象。 - 执行构造函数内部代码。
- 如果构造函数没有返回非空对象,就返回创建的新对象,否则就返回构造函数的返回值。
相比之前工厂方法创建的对象类型都为Object
,该方式可以创建特定类型的对象。
但是该方法还是有有个缺点就是如果定义了方法在构造函数中,那么每次调用构造函数都会重新的取定义该方法。最好的方法是将该方法定义在构造函数的原型中。
对象属性的相关操作
之前我们都是将属性直接定义在对象中,我们在添加属性的时候可以对属性进行一些限制,这是就需要属性描述符来进行属性的设置了:
Object.defineProperty
该方法会在对象上定义一个属性,并对这个属性进行配置,或者对对象某个现有属性进行配置,并返回该对象。
该方法有三个参数:
- 要定义属性的方法。
- 要修改的属性名。
- 一个对象,包含对该属性的一些设置。
属性描述符分为两类,一种是数据属性,一种是存取属性。
数据属性描述符
数据属性描述符有以下四个类型:
- [[Configurable]]:表示属性是否可以通过
delete
删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符。默认我们定义的属性该值为true
,当我们通过属性描述符来定义属性时该值为false
。 - [[Enumerable]]:表示属性是否可以通过
for-in
或者Object.keys()
返回该属性,默认我们定义属性该值为true
,表示可枚举的,我们通过控制台打印也可见。当我们通过属性描述符来定义就是false
。 - [[Writable]]:表示是否可以修改该属性的值,其默认值也是和上述两个属性描述符一致。
- [[value]]:表示该属性的值,默认为
undefined
存取属性描述符
也有四个类型:
前两个和之前数据属性描述符一致,后两种不一样了,是以下两种方法:
- [[get]]:当我们获取属性的时候会调用的函数,默认为
undefined
- [[set]]:当我们对属性进行重新赋值的时候会调用。默认为
undefined
注意,存取属性描述符和数据描述符不一样的两个属性是不能共存的,即get
或者set
方法不能和value
或writeable
共存。
同时定义多个属性
Object.defineProperties
可以同时定义多个属性
原型
JavaScript
中每个对象都有个内置属性 [[prototype]]
,该对象指向另外一个对象。
当我们通过一个key
来获取属性value
的时候,我们首先会在当前对象上去查找,但如果没有找到就会去访问属性 [[prototype]]
所指向的对象,再继续查询该key
。
获取原型对象
我们通过对象字面量或者Object
方式创建的对象都有一个属性__proto__
(该属性是早期浏览器自己添加的,存在一定的兼容性问题)。
还可以通过Object.getPrototypeOf
方法获取。
函数的原型
之前说了所有的对象都有一个[[prototype]]
属性指向原型对象。在js
中函数实际上也是对象,每个函数都可通过prototype
的属性来获取属性。
之前我们说过每个对象都有个[[prototype]]
属性指向原型。而函数比较特殊,有个叫prototype
的属性指向原型。所以该属性并不是函数也被称之为对象而拥有,而是正是作为函数才拥有。
constructor
原型对象上都有一个叫做constructor
的属性,该属性指向当前函数对象。
重写原型对象
如果我们要在原型上添加很多的属性,可以直接重写整个原型对象
上述修改了原型后,也会导致修改后的原型constructor
指向错误,所以我们可以修改该属性,推荐使用Object.defineProperty
方法修改该属性,并设置为不可枚举。
之前说到的构造函数的缺点,我们可以将方法定义在构造函数的原型中了,这样每次创建对象就不用重复的定义方法了。这些方法同样可以给所有创建的对象实例所使用。
继承
面向对象有三大特性:封装,继承,多态。
JavaScript
可以通过原型链来实现继承,这里先了解下原型链的含义。
首先,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取。当然Object
的原型就没有原型了,指向为null
,这也被称之为顶层原型了。
通过Object
创建的对象原型都是 [Object: null prototype] {}
,该原型上有很多方法,该原型的原型指向null
。
如果一个对象的原型是另外一个对象的实例,那么就可以通过原型进行访问另外一个对象的原型,继而查找也可以一级一级的查找,这样就形成了原型链。
通过原型链方式实现继承
但是该方式有一些弊端:
- 我们通过属性打印,并不能看到原型中的属性,所以看不见
name
属性。 - 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题,比如一个对象实例修改了
name
,那所有的对象实例获取该name
属性都是修改过的。 - 不能给
Person
传递参数,所以导致对象不能自定义。 - 创建的子类类型仍然是父类的类型。
借用构造函数继承
该方式解决了部分原型链的弊端,但是还有一些其他的弊端:
Person
构造函数被调用了至少两次。stu
的原型对象会多出来一些无用属性,这些属性在子类的原型中也有(所有的子类实例事实上会拥有两份可以访问的属性,一份在当前实例,一份在原型)。
父类原型直接赋值给子类原型
这种方式在遇到对子类元素的原型上加上方法时会导致方法加到父类的原型上,所以导致所有的继承于父类的子类都会有该方法。(上述老师是不应该有doHomework
这个方法的)。当然这解决了父类原型上有多余属性的问题,但是该方法从面向对象的角度来说不太合适。作为一个了解吧。
原型式继承
该方法也有一个弊端就是如果我要为子类添加一个属性或者其他方法,那该方法并不能对所有的实例共享。需要一个一个添加。
寄生式继承
这个方式只是定义了一个方法,将上一个原型式继承的弊端给优化了下,不用一个一个给实例添加属性。
寄生组合式继承
上述的一些方法,有的是社区提供的思路,但或多或少都有一些弊端,但是这也给了我们很多思路想法,这里展示一个较为完美的继承方式:
对象方法的补充
获取对象的属性描述符:
getOwnPropertyDescriptor
获取单个getOwnPropertyDescriptors
获取所有
禁止对象继续添加新的属性:
密闭对象,禁止对象配置/删除里面的属性:
冻结对象,不允许修改现有属性(实际上是调用seal,并将现有属性的writable: false
)
hasOwnProperty
对象是否有某一个属于自己的属性(不是在原型上的属性)
in/for in 操作符
判断某个属性是否在某个对象或者对象的原型上
instanceof
用于检测构造函数的pototype
,是否出现在某个实例对象的原型链上
这里注意instanceof
右边传入的是可执行的函数
isPrototypeOf
用于检测某个对象,是否出现在某个实例对象的原型链上。
这个和上面的一样,只是传入的是一个对象,左边是参照对象,右边是被检测的对象。
原型继承关系
以下是一张解释原型继承的关系图:
首先我们之前说的原型分为函数的原型和对象的原型。
函数的原型都可以通过prototype
进行访问,这个原型被称之为显示原型。
对象中的原型一般可以通过 __ptoto__
访问,被称之为隐式原型。
但是函数也是对象,所以它也有一个隐式原型。
但是注意,一般的函数这两个原型是不相等的:
解释:
Foo
作为对象:其原型是Function
构造函数的原型。
Foo
作为函数:其原型就是Foo.prototype = { constructor: Foo }
。
当然要注意构造函数Function
,其显示原型和隐式原型都是一样的。
可以这么理解:
ES6的class
了解了js
的对象和继承之后可以看到js
面向对象其实书写起来并不简洁,而es6
就引入了class
关键字来直接定义类。
当然class语法只是一个语法糖,遇到低版本浏览器,还是需要babel
转译成es5
语法才会被正常运行。
基本使用
定义方法:
这里我们了解babel
转换后的代码:
继承
继承也是实现了extends
方法,本质上还是采用了寄生组合式继承:
babel
转换后的代码:
创建类继承自内置类
我们默认创建的类其实是继承了内置的Object
。
完整的写法为:
另外我们还可以通过继承对内置的类进行扩展
类的混入
在JavaScript
中类只支持单继承,但是我们要实现多继承的话只能自己模拟:
多态
传统的面向对现象的多态需要满足三个前提:
- 必须要有继承。
- 子类需要重写父类方法。
- 必须有父类引用指向子类对象。
JavaScript
中的多态定义相对要灵活些: 当对不同的数据类型执行同一个操作时, 如果表现出来的行为(形态)不一样, 那么就是多态的体现。
传统的多态:
JavaScript的多态:
8.ES6/7/8/9/10/11/12...基本语法
字面量的增强
字面量的增加包括:
- 属性的简写。
- 方法的简写。
- 计算属性的简写。
解构
结构分为数组的解构和对象的解构。
数组解构
数组结构是有顺序区分的
对象解构
对象解构没有顺序区分
let/const
let/const
声明的变量不能重新声明。const
声明的变量不允许修改。
关于let/const
与作用域的关系,,我们如果在声明前了使用let/const
所定义的变量,会报错。这与var
有着很大的区别。
详细的描述可以查看 ECMAScript 2015 Language Specification – ECMA-262 6th Edition
其中写道:使用let/const
声明的变量实在其包含的词法环境被实例化时创建,但是在这些变量的词法绑定被声明前,它们不能以任何方式被访问。
当然对于这样的处理是否是一种作用域提升,官方并没有实际的说明。
另外注意,let/const
创建变量是不会被放到window
中的。
最新的ECMA标准中对于上下文的描述如下:
我们将变量对象称之为变量环境(VE
),里面的属性和函数声明称之为环境记录。每一个执行上下文会被关联到一个变量环境。
但是对于这个变量环境对象是否是window
或者其他对象就需要看JS引擎的实现了,比如v8
引擎是通过过VariableMap
的一个hashmap
来实现它们的存储的。
而window
对象是早期的GO对象,在最新的实现中其实是浏览器添加的全局对象,并且 一直保持了window
和var
之间值的相等性。
块级作用域
在es5
中只有两个块级作用域:函数作用域和全局作用域。
在ES6中新增了块级作用域,并且通过let、const、function、class
声明的标识符是具备块级作用域的限制的。
另外注意函数虽然拥有块级作用域,但是一个在块级作用域的函数声明仍然可以在外部被访问(不同的浏览器有不同实现的)。
总之,现在的开发不推荐使用var声明变量,在使用let/const
的时候也是首先选择const
,如果确定变量之后会被改变那就该使用为let
。
字符串模板
模板字符串
es6
之前我们对于对象和字符串的拼接是十分麻烦的,而且遇到换行还更加不好拼接。
标签模块字符串
第一个参数是完整的字符串(被变量所分割为数组),剩余的参数就是模板字符串中的变量了。这个语法在React
的一个css in js(styled-components)
库中有被应用。
函数的默认参数
对应的参数传入了就使用传入的参数,否则就使用默认
另外默认值会改变函数的length
的个数,默认值以及后面的参数都不计算在length
之内了
函数的剩余参数
这其实是一种代替arguments
的方式,注意剩余参数的表达式要放在函数参数的最后。
展开语法
以下场景使用展开语法
- 函数参数中
- 构建对象字面量
- 数组构造时
注意构建对象的时候,如果之后添加的属性在之前的展开对象中那是会覆盖掉之前的属性。
而且,注意展开运算符其实是一种浅拷贝。
数值的表示
es6
之后的版本都对数值进行了规范。
Symbol
以往的对象中的属性名其实一直是字符串,而我们向一个对象中添加属性可能会导致覆盖已有的属性,特别时我们使用第三方库的对象或者在开发中使用别人定义的对象。
我们使用Symbol
来定义属性名就能有效的避免这个问题了
基本使用
添加描述
我们可以看到上述两个Symbol
类型的变量打印结果都是一样的,所以为了区分,我们可以在创建的时候添加一个参数作为描述来对变量进行标识。
作为对象的key
创建一样的Symbol
在key
相同的情况是相同的
Set
在ES6中新增了两种数据结构,一种是set
一种是map
。
set
可以用来保存数据类似于数组,但是set
中的数据不能有重复的存在。
创建set
Set
是一个构造函数:
常见方法
适用场景
set
中不能存在重复的元素,我们可以利用这一特性为数组进行去重。
WeakSet
该数据类型其实大部分与Set
一样,有以下区别:
WeakSet
中只能存放对象类型,不能存放基本类型。WeakSet
中对于对象的引用是弱引用,如果没有其他引用对该数据类型中的某个对象进行引用,那么该对象就会被垃圾回收掉。- 不能被遍历,存储到
WeakSet
中的元素是不能被获取到的。
常见方法
适用场景
对于该数据类型有个概念叫弱引用,我们先讲解下弱引用是什么回事。
我们通过字面量创建一个对象,如下:
obj
这个标识符就保存着这个对象的引用地址,并且我们称这种引用为强引用,垃圾回收机制在执行回收的时候检查到有obj
引用着,就不会对该对象进行回收。
而我们将该对象放入WeakSet
中:
此时WeakSet
内部就有一项对该对象形成引用关系,但是该引用关系是弱引用。如果我们将标识符obj
置为null
,即切断obj
对对象的引用,我们可能会以为WeakSet
中还有对该对象的引用,所以垃圾回收是不会回收该对象的,但是由于垃圾回收是不管对象弱引用关系的,只会检查该对象和obj
的强引用关系被切断了,所以照样进行回收。
打个比方就是老板和你是好朋友,老板的宝马也会经常让你开,但是有一天老板因为偷税进去了,还没收了宝马车,不仅老板开不了了,你当然也开不了了。你和这个车的关系就像弱引用一样。
当然接下来展示一个WeakSet
的使用案例:
Map
Map
用于存储key--value
的映射关系,普通的对象中我们只能使用字符串或者Symbol
类型来作为属性的key
。而Map
支持任意类型作为key
。
创建方法
常用方法
WeakMap
也是和WeakSet
类似,和Map
有以下区别:
key
只能使用对象。key
对于对象是弱引用。- 不能遍历。
常用方法
比Map
要少些方法
set
get
has
delete
适用场景
Vue
的响应式对于依赖关系的存储
includes
ES7
新增的数组方法
以往我们如果需要判断一个数组中是否包含某个元素,需要通过indexOf
获取结果。但是使用indexOf
有一些缺点:
indexOf
不能判断含有NaN
的情况。indexOf
查找不到的时候返回的是数值-1这其实不太符合规范。indexOf
不能判断稀疏数组。
includes
就能弥补上述的indexOf
缺点,另外这两个方法数组和字符串都有,而且还可以指定第二个参数----position
从当前数组的哪个索引位置开始搜寻,默认值为 0
。
指数运算符号
计算数字的乘方可以通过Math.pow
来进行,ES7允许我们使用**
来计算乘方。
Object.keys
获取对象的所有key
。
Object values
我们可以通过Object.keys
获取一个对象的所有key
,在ES8中我们可以通过Object.values
获取所有的value
值。
Object entries
获取对象的key,value
,组成一个数组然后放入一个数组中
String Padding
对一个字符串进行填充操作
padStart/padEnd
两个方法只是填充的方法不一样,函数的第一个参数是当前字符串要填充到的长度,如果小于或者等于就返回当前字符串。
第二个参数是填充字符串,如果填充的字符串超过了目标长度就会截取字符串,优先保留左边的以满足目标长度。
Trailing Commas
允许我们在定义函数参数或者传参的时候在尾部添加一个逗号
这个只是为了满足一些程序员的习惯吧,当然我的编辑器默认格式化就会去掉尾部的逗号,另外我在定义对象属性的时候习惯在最后一个属性值后边加上一个逗号。
flat
该方法可以将一个数组按照指定的深度递归铺平,就是将一个数组降维,参数是降维的维度数,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
faltMap
该方法可以看成组合方法,对一个对象先map
然后再进行flat
,最后返回一个新数组。
Object fromEntries
该方法是将一个entries
数组转为普通的对象。
trimStart/trimEnd
去除字符串开头或者尾部的空格
bigInt
BigInt
是一种内置对象,它提供了一种方法来表示大于 2^53 - 1
的整数。这原本是Javascript
中可以用 Number
表示的最大数字。BigInt
可以表示任意大的整数。
如果我们用普通的number
保存大数进行运算会发生意想不到的情况的:
空值合并类型操作符
以前使用 ||
有个问题就是判断条件是转为boolean
来进行判断
对于前一个值是空字符串也是直接返回后一个值,而??
是前面为null
,undefined
才会返回后一个值。
可选链
我们通过对象获取属性操作符.
的时候,如果我们从一个null
或者undefined
获取属性值,这时会报错的。
通过?.
来获取属性,如果对象为undefined
就不会继续获取了
xxx赋值操作符
ES12新增了部分逻辑赋值操作符:
- 逻辑或赋值 ||=
- 逻辑与赋值 &&=
- 逻辑空赋值 ??=
Global this
js
可以运行在不同的环境上,但是不同环境的全局对象是有所不同的。
比如浏览器里面全局对象window
可以通过this
获取。
但node
中需要通过global
获取
所以GlobalThis
就是为了同一全局对象获取方式。
浏览器:
node中:
FinalizationRegistry
可以指定注册一个对象,并指定一个回调函数。当该对象被垃圾回收的时候执行回调函数。
注册对象
在浏览器中执行下这个代码,垃圾回收可能会等待几秒。
WeakRefs
如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用,但是可以通过WeakRefs
来创建弱引用。
实例1:
实例2:
Proxy
ES6新增了一个类Proxy
,该类可以创建一个对象的代理对象,我们可以通过代理对象来监听所有对于该对象的操作,也可以通过代理对象来进行对原对象的所有操作。
创建代理对象
Proxy的捕获器
当然Proxy
有13个对象操作的捕获器。捕获器如下:
创建一个可撤销的
Proxy
对象。
handler
对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy
的各个捕获器(trap)。
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
Object.getPrototypeOf
方法的捕捉器。Object.setPrototypeOf
方法的捕捉器。Object.isExtensible
方法的捕捉器。Object.preventExtensions
方法的捕捉器。handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor
方法的捕捉器。Object.defineProperty
方法的捕捉器。in
操作符的捕捉器。属性读取操作的捕捉器。
属性设置操作的捕捉器。
delete
操作符的捕捉器。Object.getOwnPropertyNames
方法和Object.getOwnPropertySymbols
方法的捕捉器。函数调用操作的捕捉器。
new
操作符的捕捉器。
另外注意其中的construt
和apply
。我们可以对于个函数进行创建代理对象,而它调用方式的不同就会触发不同的触发器。
Reflect
Reflect
是ES6
新增的内置对象,主要提供了很多操作对象的API,其实大部分类似Object
中的API。
其实内置对象主要是让对象API设计的更加规范而不是所有都集中在Object
上。
基本使用
Receiver的作用
在使用proxy
的get
和set
拦截方法时,其方法有一个参数叫receiver
,接下来讲解下其作用:
Reflect中的constructor方法
该方法类似于new
,和Object.create()
有些类似
MDN详细介绍:Reflect.constructor
Promise
Promise
是一个异步解决方案,是一个构造函数,类。
在以往我们使用一些异步请求的时候往往是通过回调函数来接受结果,比如以下代码:
如果是简单的请求使用这种倒没有什么问题,但是如果遇到多个请求需要等前一个请求结果到达后再发送一个请求,这时候写法上就会出现回调函数的嵌套,这对于后续的维护和可读性是十分不友好的。
所以我们使用Promise
可以将上述的代码改写为如下:
可以看到我们将嵌套写法改为了链式调用。
基本结构
Promise
在使用的过程中有三种状态:
pending
待定状态,初始状态。fulfilled
操作成功状态,执行了resolve
就会处于这个状态。rejected
操作失败状态。执行了reject
就会处于这个状态。
Executor
我们在创建Promise
的时候需要传递一个回调函数,这个回调函数就是Executor
,这个回调函数会被立即执行,并传入两个参数,resolve
和reject
。这两个参数是函数,我们就可以在这个回调函数中调用这些函数来确定当前Promise
的状态。
要注意,一旦Promise
的状态被确定,即从pending
改为其他状态就会被锁定,即不能再次改变状态。总之,Promise
只能被改变一次状态。
resolve传入不同值的区别
普通值或者对象
如果resolve
传入的是基本类型值,或者一个普通的对象,那这个值就会作为then
回调函数的属性。
Promise
如果传入的是Promise
,那这个新的Promise
的状态就会决定原Promise
的状态
thenable类型
thenable
类型指的是实现了then
方法的对象,如果resolve
传入了这样的参数那就会调用这个对象中的then
方法,根据then
方法的结果来决定Promise
的状态。
then方法多次调用
同一个Proimise
可以多次调用then
方法,这些then
方法中的回调函数都会被执行。
then 方法的返回值
then
方法本事是有返回值的,其返回值是一个Promise
,所以我们就可以进行链式调用。
then
中回调函数返回一个普通的函数,Promise
对象,或者thenable
,这些值对于返回的Promise
的状态影响和上述resolve
传入不同值的区别一样。
当then
方法抛出一个异常的时候,那返回的Promise
就处于reject
状态。
catch方法
catch
方法中的回调函数在Promise
转为reject
后执行,catch
依然可以返回一个promise,规判断后续Promise
状态规则一样。
finally
finally
是不接受参数的,无论前面的Promise
是什么状态最后都会执行。
resolve方法
我们可以直接调用Promise
类上的方法:
reject方法
和resolve
一样。
all方法
all
方法用于将多个Promise
包裹在一起组成一个新的Promise
。
新的Promise
状态由旧的Promise
共同决定:
当所有的Promise
都返回resolve
的时候,这个新的Promise
的状态就是fulfilled
,并将所有Promise
的返回值构成一个数组(顺序是保持和原来一致)作为resolve
的参数。
当有一个Promise
返回reject
的时候,新的Promise
为reject
状态,并将第一个reject
的返回值作为reject
参数。
allSettled
all
方法有一个缺陷就是当有一个Promise
为reject
后,新的Promise
就会变为reject
,那如果有在之前就已经resolved
的Promise
,以及依然处于pending
的Promise
,我们是获取不到对应结果的。
所以allSettled
就是在所有的Promise
都有结果后(无论fulfilled
还是reject
),才会有最终结果,且新的Promise
一定为fulfilled
状态。
race方法
用法和all
和allSettled
一样。当多个Promise
中有一个先有结果就返回这个结果作为参数。
any方法
用法和上述的all
,race
等方法一样。会等待一个fulfilled
状态,才会决定新的Promise
状态,当所有的Promise都为reject状态的时候就会报一个AggregateError
错误
迭代器
迭代器适用于确实用于在容器对象上遍历访问的对象,用户无需关心容器对象内部的实现细节。
总之迭代器就是用于帮助我们对某个数据结构进行遍历的对象。
JavaScript
中,迭代器也是一个具体的对象。其有以下要求:
一个无参或者有一个参数的函数,返回一个拥有两个属性的对
done
,一个boolean
类型的属性,如果迭代器可以产生序列中的下一个值,则为false
,如果迭代器已经迭代完毕就返回true
value
迭代器返回的任何JavaScript
值。done
为true
时可以省略。
一个基本的迭代器:
可迭代对象
这个和迭代器对象是不同的概念,可迭代器对象指的是实现了迭代协议的对象,即实现了@@iterator
方法,在代码中我们可以使用Symbol.iterator
来访问该属性。
当对象变为可迭代对象的时候我们就可以对该对象进行一些迭代操作,比如执行for of
语句时就会调用对象中的@@iterator
迭代器方法。
JS原生的很多对象都已经实现了可迭代协议,所以就可以用来进行迭代操作。比如:String
、Array
、Map
、Set
、arguments
对象、NodeList
集合等。
可迭代对象使用场景
for of
- 展开语法
- 解构语法
...
自定义可迭代对象
还可以设置中断监听函数,只需要在返回的迭代器对象中实现return
方法即可
生成器
生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。
生成器实际上也叫做特殊的迭代器,生成器函数执行会返回一个迭代器,生成器内部可以使用yield
来控制执行流程。
定义方式
执行流程
当我们执行next
方法,函数就会运行到yield
前暂停执行,再执行next
就会继续,再次遇到yield
就会再次暂停,如果遇到return
就会终止执行。
生成器传递参数
我们可以给生成器返回的迭代器中的next
方法传递参数。
注意我们第一次执行next
防范给其传递的参数时会被忽略的。从第二个next
开始,传递给next
的参数会作为上一个yield
的返回值。
提前结束return
return
传值后生成器结束执行,后续的next
不会继续生成值了
throw异常抛出
我们可以调用throw
在函数体外抛出异常,在生成器内部捕获异常,但是在catch
语句中并不能继续yield
,在外部的yield
可以继续。
生成器替代迭代器
异步代码请求方案
我们可以使用生成器来执行一些异步代码。下面模拟了async
和await
的实现原理。
async和await
async用于声明一个异步函数。
异步函数的执行流程
异步函数内部代码执行流程和普通函数是一致的,默认情况也是同步执行。
异步函数特点
异步函数和普通函数的区别就是异步函数的返回值是一个Promise
:
异步函数内部如果有异常时,那么程序它并不会像普通函数一样报错,而是会作为Promise
的reject
来传递:
await
await
只能在async
定义的异步函数中使用,通常await
后跟上一个表达式,这个表达式会返回一个Promise
,那await
会等待后面Promise
的状态变化为fulfilled
后再继续执行异步函数中剩余的代码。
如果await
后面跟的是一个普通的值,那就会直接返回这个值。
如果跟的是一个thenable
,那就会调用或者对象中的then
方法来决定值。
如果后面跟的是一个Promise
,且状态为reject
,那将会将这个reject
结果作为整个异步函数Promise
的reject
值。
事件循环
进程和线程
首先介绍下进程和线程:
进程是正在执行程序的一个实例。操作系统负责管理正在运行的进程,并为每个进程分配特定的事件来占用CPU,分配特定的资源。
线程是进程中的单条流向,也可以指操作系统能够运算调度的最小单位。线程也具有进程中的部分属性,所以线程也可以称之为轻量型的进程。比如浏览器是一个进程,每个tab
就可以看作一个一个的线程。
事件循环基本概念
我们的JS代码是在一个单独的线程中执行的,如果代码中有非常耗时的操作那当前线程就会被阻塞。所以有些耗时的操作是放置其他的线程的调用栈中来执行,比如定时器和网络请求。JS主线程就会继续执行后续的同步代码。当其他线程中调用栈中的函数开始执行的时候就会将回调函数放入事件队列中,这个事件队列在有回调函数的时候就会通知JS主线程从这个队列中依次取出执行。上述的整个过程就称之为事件循环。
宏任务和微任务
在事件循环中其实维护着两个事件队列,分别称之为宏任务队列,微任务队列。
我们所执行的一些耗时事件会分别放置在这两个队列中,比如宏任务队列中放置ajax,定时器,DOM监听,UI rendering等。微任务队列放置Promise的then回调,Mutation Observer API,queueMicrotask() 等。
事件循环对于两个队列的优先级执行顺序的规范是主线程的JS代码先执行,然后再执行事件队列中的任务(回调),但是在执行宏任务队列前要保证微任务队列是空的,即要先执行完微任务队列中的任务。
Node中的事件循环
事件循环其实在不同的平台和浏览器实现上有些不同,但整体的效果基本一致,事件循环就好比一个桥梁。在Node
平台中事件循环主要作为JS代码的系统调用的通道。
关于Node
的事件循环教程可以参阅网址
Node
目前的事件循环主要参照libuv
这个异步IO库实现。
Node
中一次完整的事件循环叫做一个tick
,它又分为很多个阶段,一般由如下的阶段组成:
- 定时器(timer):定时器中的回调函数。
- 待定回调(pending callback)一些系统操作执行回调。
- idle,prepare 仅系统使用。
- 轮询(poll)检索新的I/O事件,执行与I/O相关的一些回调。
- 检测(check)setImmediate()回调函数执行。
- 关闭的回调函数
所以在node
处理宏任务和微任务队列时,也不和浏览器一样只是维护着两个队列。node
维护的队列更加的多。
所以就会按照如下顺序执行代码:
错误处理
在开发中我们需要对使用者传递的参数做一些校验,如果用户没有按照我们的要求去传递对应的参数我们应该给用户一个错误提示,这就需要我们主动去告知用户。
throw
我们可以通过throw
去抛出一个异常来告知用户。
throw
可以跟上基本类型和对象类型,通常我们会抛出一个对象类型,这样会包含更多信息。
对于对象类型,我们可以自己封装一个error
类也可以使用系统自带的Error
类。
会打印更多的信息:
我们使用默认系统创建的error对象包含三个属性:
- messsage:创建
Error
对象时传入的message
。 - name:
Error
的名称,通常和类的名称一致。 - stack:整个
Error
的错误信息,包括函数的调用栈,当我们直接打印Error
对象时,打印的就是stack
。
我们一般不会去修改这些属性。
另外Error类还有一些子类:
- RangeError:下标值越界时使用的错误类型。
- SyntaxError:解析语法错误时使用的错误类型。
- TypeError:出现类型错误时,使用的错误类型。
throw
后面的代码也是和return
一样,不会继续执行。
异常传递
我们如果在一个函数内抛出一个异常,如果调用函数的地方并没有处理异常,那就会将该异常抛出到上层,直到最顶层的函数调用。
可以看到调用栈的顺序。
异常捕获
一旦我们抛出异常,但是并没有对异常进行捕获处理,那么整个程序就会被强制终止。
我们可以使用try catch来进行捕获异常并处理。这样就不会导致我们的程序强制终止了
另外try catch
后还可以加上finally
表示最后一定会执行的语句。在ES10中,catch
后面绑定的error
可以省略。
注意:如果try
和finally
中都有返回值,那么会使用finally
当中的返回值。
模块化
esModule和commandJS的区别
区别一、首先commonJs
是被加载的时候运行,esModule
是编译的时候运行。
区别二、commandJs
输出的是值的浅拷贝,而esModule
是值的引用。
区别三、comandJs
具有缓存,在第一次被加载时,会完整运行整个文件并输出一个对象,拷贝(浅拷贝)在内存中。下次加载文件时,直接从内存中取值。
Comment here is closed