JavaScript 高级
This 指向规则
案例
function foo() {
console.log(this);
}
// 1 调用方式1
foo();
// 2 调用方式2 放入对象中调用
var obj = {
name: "why",
foo: foo,
};
obj.foo();
// 调用方式三 通过 call/apply 调用
foo.call("abc");
指向定义
this 是 js 给函数的一个绑定值。
- 函数在调用时 JavaScript 会默认给 this 绑定一个值;
- this 的绑定和定义的位置(编写的位置)没有关系;
- this 的绑定和调用方式以及调用的位置有关系
- this 是在运行时被绑定的
无严格模式下 为 window 如果打开严格模式 则为 udnefined
this 的绑定规则如下:
-
绑定一:默认绑定 PS: 没有绑定到任何对象时 & 函数定义在对象中但是被独立调用 对象也是 window
-
绑定二:隐式绑定 PS:由 JS 绑定到调用对象 指向对象
-
绑定三:new 绑定
- new 执行过程
- 1 创建空对象
- 2 修改 this 指向为空对象
- 3 执行函数体代码
- 没有显示返回非空对象时 默认返回这个对象
-
绑定四 显示绑定
-
如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用
-
function foo() {
console.log(this)
}
var obj = {
name: "why",
foo: foo
}
foo.call(123)
console 输出内容 {name: 'why', foo: ƒ} -
call/apply 可以帮助我们完成这个效果
-
额外函数补充
Call / Apply 调用方法 两者区别不大 但是又细微差别
apply
:
function foo(name, age, height) {
console.log("foo 函数this 指向", this);
console.log("参数:", name, age, height);
}
// 普通调用 直接入参
foo("why", 18, 1.22);
// apply
// 第一个参数 绑定 this
// 第二个参数 传入额外的实参 以数组的形式
// foo.apply("apply",["why", 18, 1.22])
foo.apply("123", ["why", 18, 1.22]);
call
:
function foo(name, age, height) {
console.log("foo 函数this 指向", this);
console.log("参数:", name, age, height);
}
// call
// 第一个参数 绑定 this
// 后续参数以 参数列表形式
foo.call("call", "远目鸟", 18, 12);
两者 相同处 都是调用方法 第一参数都指向 this 唯一区别只在后续传入的参数的形势
- apply 为数组
- call 为列表 以
,
分割
bind
:会创建 绑定函数 我们希望调用 foo 的时候总是让 this 指向 obj
function foo() {
console.log("foo 函数this 指向", this);
}
var obj = {
name: "why",
};
// 需求 调用foo时 总是绑定 obj
var bar = foo.bind(obj);
bar();
在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 实际开发中 使用不多 作为参考 了解即可
内置函数
一般 对于浏览器 的内置函数 或者是第三方框架的 this 指向 我们只能用经验去判断 一个一个去源码或者文档查看和并不现实
This 优先级
- 默认绑定 优先级最低
- 显式绑定 高于隐式绑定
- new 高于隐式绑定 PS:new 不能和 call/apply 一起使用
- new 绑定优先级高于 bind
- 同显式 bind 优先级高于 call/apply
拓展: 规则之外
**情况一:**如果在显示绑定中,我们传入一个 null 或者 undefined,那么这个显示绑定会被忽略,使用默认规则:
function foo() {
console.log("foo 函数this 指向", this);
}
var obj = {
name: "why",
};
foo.call(obj);
foo.call(null);
foo.call(undefined);
var bar = foo.bind(obj);
bar();
但是打开严格模式 就会可以使用基础属性 直接显示 null 或者 undefined
**情况二:**创建一个函数的 间接引用,这种情况使用默认绑定规则。
- 这种情况 (obj2.foo = obj1.foo) 会使用默认规则 指向 window
var obj1 = {
name: "obj1",
foo: function () {
console.log("foo 函数this 指向", this);
},
};
var obj2 = {
name: "obj2",
};
obj1.foo();
(obj2.foo = obj1.foo)();
**情况三:**箭头函数
- 箭头函数不会绑定 this、arguments 属性
- 箭头函数不能作为构造函数来使用
// {} 是执行体
var arrFn = () => { }
// 指向的是对象 需要加小括号才可以做到
var arrFn = () => ({ name: "why" })
箭头函数
-
基本写法
-
():函数的参数
-
{}:函数的执行体
-
var foo3 = (name, age) => {
console.log("箭头函数的函数体");
console.log(name, age);
};
-
-
优化写法
-
只有一个参数时, 可以省略()
names.forEach((item) => {
console.log(item);
}); -
只有一行代码时, 可以省略{}
names.forEach((item) => console.log(item));
-
只要一行代码时, 表达式的返回值会作为箭头函数默认返回值, 所以可以省略 return
var newNums = nums.filter((item) => item % 2 === 0);
var newNums = nums.filter((item) => item % 2 === 0); -
如果箭头函数默认返回的是对象, 在省略{}的时候, 对象必须使用()包裹 () => ({name: "why"})
var arrFn = () => ["abc", "cba"]
var arrFn = () => {} // 注意: 这里是{}执行体
var arrFn = () => ({ name: "why" })
console.log(arrFn())
-
箭头函数不使用 this 的四种标准规则(也就是不绑定 this),而是根据外层作用域来决定 this。
我们来看一个模拟网络请求的案例:
- 这里我使用 setTimeout 来模拟网络请求,请求到数据后如何可以存放到 data 中呢?
- 我们需要拿到 obj 对象,设置 data;
- 但是直接拿到的 this 是 window,我们需要在外层定义:var _this = this
- _在 setTimeout 的回调函数中使用_this 就代表了 obj 对象
- 但是如果使用箭头函数根据特性他会向上寻找 this 省去了_this = this 的操作
var obj = {
data: [],
getData: function () {
request("/11", (res) => {
this.data = [].concat(res);
});
},
};
function request(url, callbackFn) {
var res = ["abc", "cba", "nba"];
callbackFn(res);
}
obj.getData();
总结
- this 的指向问题与优先级 是踏入 JS 的敲门砖,如果不先系统了解之后使用的时候可能会出现奇怪的错误
- 使用 ES6 的语法 箭头函数 提前熟悉 ES6 语法可以提升开发效率
浏览器渲染原理
我们通过 url 进入到页面拿到资源以及获得返回资源的过程
浏览器内核
常见的浏览器内核有
- Trident ( 三叉戟):IE、360 安全浏览器、搜狗高速浏览器、百度浏览器、UC 浏览器;
- Gecko( 壁虎) :Mozilla Firefox;
- Presto(急板乐曲)-> Blink (眨眼):Opera
- Webkit :Safari、360 极速浏览器、搜狗高速浏览器、移动端浏览器(Android、iOS)
- Webkit -> Blink :Google Chrome,Edge
页面渲染流程:
浏览器的渲染页面过程
HTML 解析过程
一般情况下服务器会给浏览器返回 xx.html 文件 解析 html 其实就是 Dom 树的构建过程
我们可以根据以下 html 结构 来简单的分析出 html 的解析过程
解析 CSS 规则树
在解析的过程中,如果遇到 CSS 的 link 元素,那么会由浏览器负责下载对应的 CSS 文件:
PS: 这里下载 CSS 是不会影响到 DOM 树的解析的
下载完成后 就会对 CSS 文件解析出对应的 规则树 , 案例如下图 :
body {
font-size: 16px;
}
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
解析步骤 构建 Render Tree
当有了 DOM Tree 和 CSSOM Tree 后,就可以两个结合来构建 Render Tree 了
需要注意的是:
- link 元素不会阻塞 DOM Tree 的构建过程,但是会阻塞 Render Tree 的构建过程
- Render Tree 和 DOM Tree 并不是一一对应的关系,比如对于 display 为 none 的元素,压根不会出现在 render tree 中;
解析步骤 布局和绘制
- 渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体。
- 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息;
- 布局是确定呈现树中所有节点的宽度、高度和位置信息;
- 将每个节点绘制(Paint)到屏幕上
- 在绘制阶段,浏览器将布局阶段计算的每个 frame 转为屏幕上实际的像素点;
- 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如 img)
渲染的流程可以参考下图 :
完成以上五步 成功在浏览器渲染出 对应的 xx.html 文件
回流和重绘
回流(reflow)
reflow
:
- 我们渲染出来的节点大小位置 也就是布局时第一次渲染出之后就确定的
- 之后对于节点大小和位置重新计算的行为 叫做回流(reflow)
回流在什么时候会出现 :
- DOM 结构发生变化 (添加 & 移除)
- 改变了 CSS 样式代码 也就是布局
- 修改了 窗口尺寸
- 或者是调用了某些内置函数 获取位置和尺寸信息
重绘 (reprint)
- 我们渲染的第一次,在之前的流程图中叫做 ==printing==
- 在之后需要重新渲染的时候 成为重绘
重绘怎么出现 :
- 修改 CSS 如 颜色 文字样式
拓展思路
- 只要出现回流 就一定会引起重绘 其实看到上述的解释 也很容易就发现 回流也是在出发样式代码或者改变的时候触发
- 回流的性能并不好 也很明显 重新渲染整个 DOM 很浪费性能
总结
- 修改样式 尽可能减少回流次数 也就是设计好之后,非必要不去改动样式和 DOM 的结构
- 避免频繁使用 JS 去操作 DOM
- 尽可能减少函数获取储存位置的信息
特殊解析 - composite 合成
绘制的过程,可以将布局后的元素绘制到多个合成图层中。
会形成新的合成层的属性:
- 3D transforms
- video、canvas、iframe
- opacity 动画转换时
- position: fixed
- will-change
- animation 或 transition 设置了 opacity、transform
PS:分层确实可以提高性能,但是它以内存管理为代价,所以不作为性能优化策略来使用
script 元素和页面解析的关系
JS 在我们渲染过程中的那一步呢?
- 在渲染 html 的时候 js 没有继续构造 DOM 的能力
- 如果需要需要的部分 会先停止构建,下载 js 执行脚本
- 把需要构建的东西构建完成后 继续执行构建 DOM
这么做有什么好处?
- JS 有操作和修改 DOM 的作用
- 为什么会先去执行 js 脚本? 因为之前提到了 回流时很吃性能的所以最好一次性弄好 减少不必要的回流
代码案例
index.html
<script src="./js/test.js"></script>
<body>
<div class="box"></div>
</body>
<script>
var boxel = document.getElementsByClassName("box");
console.log(boxel);
</script>
test.js
debugger;
console.log("hello");
新的问题:
- 在现在的开发模式中 大多都是使用 vue 和 React 作为开发框架 JS 的占比往往很大 处理事件也会变长
- 这也导致了 如果解析阻塞 那么在脚本解析完成之前 可能界面什么都不显示
这里 js 给我们提供了两个属性 来解决这个问题
defer 属性
defer 属性告诉浏览器不要等待脚本下载,而继续解析 HTML,构建 DOM Tree,如果脚本提前下载好就等待加载,等 DOM 完成 在触发 DOMContentLoaded 之前执行 defer 中的代码
PS: defer 按照默认顺序执行 不会影响顺序 且可以操作 DOM
<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
})
</script>
<script>
var boxel = document.getElementsByClassName("box")
console.log(boxel);
</script>
<script defer>
console.log("defer-demo")
</script>
<script>
debugger
console.log("hello")
</script>
建议:
- 将 defer 放入 head 中使用 这个属性的特性放在末尾 就本末倒置了
- defer 只对外置脚本有效果
async 属性
async 特性与 defer 有些类似,它也能够让脚本不阻塞页面。
它的特性:
- 浏览器不会因 async 脚本而阻塞(与 defer 类似);
- async 脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本
- async 不会能保证在 DOMContentLoaded 之前或者之后执行
<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
})
</script>
<script async>
console.log("defer-demo")
</script>
总结
- defer 通常用于文档解析操作 DOM 且有顺序要求的 JS 代码
- async 通常用于独立脚本 可以理解为没有什么依赖的脚本 如果有依赖 那么不保证一定能提前加载到
总结
- 首先时了解和认识一些浏览器的内核
- 了解从服务器加载 到渲染页面的流程
- 细化每一步的大致内容
- 发现有问题且探索到问题的一些解决方法
JS 运行原理
深入了解 V8 引擎原理
浏览器内核是由两部分组成的,以 webkit 为例:
- WebCore:负责 HTML 解析、布局、渲染等等相关的工作;
- JavaScriptCore:解析、执行 JavaScript 代码;
官方对 V8 引擎的定义:
- V8 是用 C ++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chrome 和 Node.js 等
- 它实现 ECMAScript 和 WebAssembly,并在 Windows 7 或更高版本,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理 器的 Linux 系统上运行。
- V8 可以独立运行,也可以嵌入到任何 C ++应用程序中。
V8 引擎的架构很复杂 ,我们可以先了解它庞大引擎的一些模块
- Parse 模块会将 JavaScript 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码
- 如果函数没有被调用,那么是不会被转换成 AST
- Parse 的 V8 官方文档:https://v8.dev/blog/scanner
- Ignition 是一个解释器,会将 AST 转换成 ByteCode(字节码)
- 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 如果函数只调用一次,Ignition 会解释执行 ByteCode
- Ignition 的 V8 官方文档:https://v8.dev/blog/ignition-interpreter
- TurboFan 是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码
- 如果一个函数被多次调用,那么就会被标记为热点函数,它会被 TurboFan 转换成优化的机器码,提高代码的执行性能
- 机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执 行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码
- TurboFan 的 V8 官方文档:https://v8.dev/blog/turbofan-jit
V8 架构解析图 来自官方
解析代码的步骤:
- 获得到代码之后 V8 用流输入通过词法分析,分析成 token
- 解析/预解析 来生成一个一个执行节点
- 生成 AST 树
- 转成字节码 如果有热点方法就会走 turbofan 编译器优化成机械码提升性能
全局代码执行过程
js 引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象 所有的作用域(scope)都可以访问
- 里面会包含 Date、Array、String、Number、setTimeout、setInterval 等等
- 其中还有一个 window 属性指向自己
js 引擎内部有一个执行上下文栈(Execution Context Stack,简称 ECS),它是用于执行代码的调用栈,
他执行的式全局代码块,它的作用就是:
- 为了执行代码构建一个 Global Execution Context GEC 全局上下文
- 将这个构建的上下文加入到执行栈中 也就是将 GEC 放入 ECS 中
GEC 被放入到 ECS 中里面包含两部分内容:
- 在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 GlobalObject 中,但是并不会赋值
- 在代码执行中,对变量赋值,或者执行其他的函数;
每一个执行上下文会关联一个 VO(Variable Object,变量对象),变量和函数声明会被添加到这个 VO 对象中,当全局代码被执行的时候,VO 就是 GO 对象了
全局上下文三个关键:
- VO(go)
- 作用域链
- This
执行以下代码过程
var message = "Global Message";
function foo() {
var message = "Foo Message";
}
var num1 = 10;
var num2 = 20;
var res = num1 + num2;
console.log(res);
全局代码执行前
执行代码后
函数代码执行过程
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称 FEC),并且压入到 EC Stack 中
- 当进入一个函数执行上下文时,会创建一个 AO 对象(Activation Object)
- 这个 AO 对象会使用 arguments 作为初始化,并且初始值是传入的参数
- 这个 AO 对象会作为执行上下文的 VO 来存放变量的初始化
如下函数执行过程
执行前
执行后
流程为:
- 执行前创建 FEC 也就是函数执行上下文
- 创建 AO 对象 name 为函数名
- 创建作用域链
- 生成函数对象存放代码
- thisbing(暂无)
- 之后从上到下执行代码
- 执行完成后将 name 变为 undefined
作用域和作用域链
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)
- 作用域链是一个对象列表,用于变量标识符的求值
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
PS : 作用域会提升 在本身 vo 没有情况下 会去上层寻找,我们先输出后声明会输出 undefined, 这里也印证了
作用域提升小练习
var n = 100;
function foo() {
n = 200;
}
foo();
console.log(n);
N =200
顺序内存查找图如下 :
- 全局代码创建函数 找到 n 放入到函数 vo 中 之后调用 foo()
- 在函数调用后找到 GO 中的 n 复制
- 函数结束,之后输出 n
作用域链也是我们 JS 闭包的一个重点, js 中闭包就是通过作用域链的方式来完成变量可以跨作用域访问的,为我们加快提升了开发的效率 也省去很多麻烦
JS 内存管理
内存原理:
任何变成语言在执行的时候都需要操作系统来分配内存,只是有些语言需要手动管理分配的内存有些语言有专门来管理内存的方式 如 JVM
了解以上的概念之后,我们再来了解一下大致的内存周期
- 分配需要的内存
- 使用内存
- 在不使用的时候释放内存
JS 属于自动管理内存的语言
在我们定义数据的时候 JS 会给我们分配内存,但是内存分配的方式有区别
- 对于原始数据内存分配在执行的时候 直接放在栈空间进行分配
- 对于复杂的数据类型 会在堆内存中开辟一块空间 并且将这块空间的指针返回值变量引用
垃圾回收机制算法
概念:
因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。
对比手动管理内存释放语言 对于开发者的技术要求非常高,一旦操作不但 效果反而会变得很差,这个也形成了高手可以做到性能很高 但是苦于进阶的选手,所以现在大部分高级语言都实现了 GC 也就是垃圾回收机制/垃圾回收算法
GC 怎么知道哪些对象是不再使用的呢?
对于 GC 的实现是百花齐放的 设计语言的人总能整出花活,这里介绍几个常见的 GC 算法
常见 GC - 引用计数(Reference counting)
- 当一个对象有一个引用指向它时,那么这个对象的引用就+1;
- 当一个对象的引用为 0 时,这个对象就可以被销毁掉;
PS: 这个算法的弊端就是会产生循环引用 就是加入 a b 之间互有属性引用 会出现两个对象哦都无法销毁的问题
常见的 GC 算法 – 标记清除(mark-Sweep)
这个算法的核心思想是实现可达性
设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象
PS:这个算法可以很好的解决循环引用的问题
- 他会从一个根对象去不断查找确认查找之后就会标记对象
- 如果发现找不到 就等于无法引用 那么就会去销毁(如下图)
- 前提是 RO 对象不会被删除 其实就代表我们 js 中的 window 对象
拓展
其他的 GC 算法
- 标记整理算法(Mark-Compact) 回收的时候保留存储对象搬运到灰级连续的内存空间,整合空闲空间,避免内存碎片化
- 分代收集(Generational collection) 对象分为旧 新 两组 有很多对象在完成工作后就会销毁 长期存活的对象变为
老旧
同时他们的检查频次不会那么频繁 - 增量收集(Incremental collection)
- 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟
- 所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的延迟
- **闲时收集(Idle-time collection)**垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
闭包概念
闭包是 JavaScript 中一个非常容易让人迷惑的知识点
JS 作为高级语言 是支持函数式编程的,这意味着在 js 中
- 函数操作和使用都非常灵活
- 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用
所以 JavaScript 存在很多的高阶函数,我们可以自己编写高阶函数,也可以使用内置的函数
在未来开源框架中也都是趋向于函数式编程
闭包的定义
- 最早出现的闭包是 Scheme
- 闭包实际上是一种存储了函数和关联环境的结构体
- 他和函数最大的区别就是闭包被捕捉的时候,他的自由变量会被锁定 即使脱离了捕捉时的上下文也可以照常运行
他的作用就是让我们可以在函数中访问到外围的变量,替我们省去了很多繁杂的变量处理
闭包小案例
function createAdder(count){
funtion adder(num){
return count+num
}
return adder
}
var adder5 = createAdder(5)
adder(100) // 100+5
这个例子可以很容易的看出闭包的使用和带来的好处
PS: 使用闭包的时候最好是可以将不需要的函数或者属性置为 null 来帮助 GC 回收释放对象 ,否则内存泄露会加大内存的占用
浏览器对于闭包的优化: 使用闭包的时候 浏览器会将我们没有使用的多余属性释放来增加性能
JS 增强
JS 函数增强
函数属性
JavaScript 中函数也是一个对象,那么对象中就可以有属性和方法,他有一些默认的属性
- name 函数名
- length 函数参数个数(ES6
...
语法不会被算在内) - arguments 类似数组对象 可以 i 用索引来获取对象
- rset
PS: 箭头函数不绑定 Arguments 对象
arguments 转为数组对象常见方法
普通的方法 就是将内容一个一个迭代到新数组了
let newArray = [];
// arguments
function foo1(m, n) {
for (var arg of arguments) {
newArray.push(arg);
}
// arguments类似数组的对象(它可以通过索引来获得对象)
console.log(newArray);
}
foo1(1, 2);
ES6 中的方法
- Array.form() 传入一个可迭代对象就可以转为数组
- 对象结构
...
的方式来复制
// 方法2
var newArray1 = Array.from(arguments);
// 方法3
var newArray = [...arguments];
rset
如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
function foo1(m, n, ...arg)
- arguments 对象包含了传给函数的所有实参但是不是数组对象 需要转换
- rest 参数是一个真正的数组,可以进行数组的所有操作
- arguments 是早期为了方便去获取所有的参数提供的数据结构,rest 参数是 ES6 中提供并且希望替代 arguments 的方案
纯函数理解和应用
副作用:
执行函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
纯函数的理解
- 输入 相同值的时候产生同样的输出 所以纯函数不能通过闭包的特性调用上层属性,因为会随着上层属性变化函数输出内容
- 函数的输出和输入值以外的信息无关和设备的外部输出也无关
- 这个函数不能有语义上可观察到的 “副作用”
纯函数辨别案例
- slice:slice 截取数组时不会对原数组进行任何操作,而是生成一个新的数组
- splice:splice 截取数组, 会返回一个新的数组, 也会对原数组进行修改
var names = ["abc", "nba", "nbc", "cbd"];
var newNames = names.slice(0, 2);
var newNames1 = names.splice(0, 2);
console.log(newNames);
console.log(newNames1);
纯函数的优势
- 稳定,可以放心使用
- 保证函数的纯度 简单的实现自己的业务逻辑,和外置的各种因素依赖关系少
- 用的时候需要保证输入的内容不被任意篡改,并且需要确定输入一定会有确定的输出
柯里化的理解和应用
函数式编程重要概念,他是一个作用于函数的高阶技术,在其他的编程语言也有使用
只传递函数部分参数来调用,让它返回一个函数去处理剩余的参数这个过程就被成为柯里化
// 普通的函数
function foo(x, y, z) {
console.log(x + y + z);
}
foo(10, 20, 30);
// 柯里化的结果
function kelifoo(x) {
return function (y) {
return function (z) {
console.log(x + y + z);
};
};
}
kelifoo(10)(20)(30);
//箭头函数写法
var foo2 = (x) => (y) => (z) => {
console.log(x + y + z);
};
自动柯里化函数
// 需要转化的例子
function sum(num1, num2) {
console.log(num1 + num2);
return num1 + num2;
}
// 自动柯里化函数
function hyCurrying(fn) {
// 1 继续返回一个新的函数 继续接受函数
// 2 直接执行 fn 函数
function curryFun(...args) {
if (args.length >= fn.length) {
// 执行第二种操作
return fn.apply(this, args);
} else {
return function (...newArgs) {
return curryFun.apply(this, args.concat(newArgs));
};
}
}
return curryFun;
}
// 对其他函数柯里化
var sumCurry = hyCurrying(sum);
sumCurry(10)(5);
sumCurry(10, 5);
柯里化函数只有在某些特殊的场景才需要使用。他得性能并不高也可能引起闭包的内存泄漏所以使用的时候需要注意。
组合函数理解和应用
当我们需要嵌套调用两个函数的时候,为了方便复用,我们可以写一个组合函数
var sum = pow(double(12));
我们可以编写一个通用的组合函数来让我们使用组合函数更加的便捷,其实思路就是很简单的将函数放入数组判断边界顺序执行
function sum(num) {
return num * 2;
}
function pow(num) {
return num ** 2;
}
function composeFn(...fns) {
// 边界判断
var length = fns.length;
if (length < 0) {
return;
}
for (let i = 0; i < length; i++) {
var fn = fns[i];
if (typeof fn != "function") {
throw new Error(`index postion ${i} must be function`);
}
}
//轮流执行函数 返回结果对象
return function (...args) {
var result = fns[0].apply(this, args);
for (let i = 1; i < length; i++) {
var fn = fns[i];
result = fn.apply(this, [result]);
}
return result;
};
}
var newfn = composeFn(sum, pow);
console.log(newfn(5)); //100
with 语句、eval 函数(拓展知识)
with
语句 扩展一个语句的作用域链,不推荐使用有兼容性问题
eval
允许执行一个代码字符串。他是一个特殊函数可以将传入的字符串当作 js 代码执行
- 可读性差
- 有注入风险
- 必须经过解释器 不会得到引擎的优化
严格模式的使用
js 的局限性 :
- JavaScript 不断向前发展且并未带来任何兼容性问题;
- 新旧代码该新模式对于向下兼容有帮助但是也有问题出现
- 就是创造者对于 js 的不完善之处会一直保留
ES5 标准中提出了严格模式的概念,以更加严格的方式对代码进行检测和执行
只需要在代码的开头或者函数的开头 加入use strict
就可以开启严格模式
JS 对象增强
数据属性描述符
我们的属性一般定义在对象的内部或者直接添加到对象内部,但是这种方式我们就不能对属性进行一些限制,比如这个属性是否是可以通过 delete 删除,是否可以 for-in 遍历的时候被遍历出来等等
PS: 一个属性进行比较精准的操作控制,就可以使用属性描述符。
- 通过属性描述符可以精准的添加或修改对象的属性
- Object.defineProperty 来对属性进行添加或者修改
这个方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
Object.defineProperty();
属性描述符分类
分为两类:
- 数据属性
- 存取属性
数据属性描述符
Configurable
:表示属性是否可以通过 delete 删除属性,是否可以修改它的特性
- 使用对象定义属性的时候为 true
- 使用属性描述符来定义的时候 默认为 false
Enumerable
:表示属性是否可以通过 for-in 或者 Object.keys()返回该属性;
- 直接对象内定义的时候 为 true
- 通过属性描述符定义为 false
Writable
:表示是否可以修改属性的值;
- 直接对象内定义的时候 为 true
- 通过属性描述符定义为 false
value
:属性的 value 值,读取属性时会返回该值,修改属性时,会对其进行修改
- 默认情况下这个值是 undefined
使用案例
var obj = {
name: "whit",
age: 12,
};
Object.defineProperty(obj, "name", {
configurable: false,
enumerable: false,
writable: false,
value: "1234",
});
存取属性描述符
Configurable&Enumerable
也是存取属性描述符
get
:获取属性时会执行的函数。默认为 undefined
set
:设置属性时会执行的函数。默认为 undefined
同时定义多个属性
// 多个属性调用
Object.defineProperties(obj, {
name: {
configurable: false,
enumerable: false,
},
age: {
enumerable: false,
writable: false,
},
});
对象方法补充
获取对象的属性描述符:
- getOwnPropertyDescriptor
- getOwnPropertyDescriptors
禁止对象扩展新属性:preventExtensions
- 给一个对象添加新的属性会失败(在严格模式下会报错);
密封对象,不允许配置和删除属性:seal
- 实际是调用 preventExtensions
- 并且将现有属性的 configurable:false
冻结对象,不允许修改现有属性: freeze
- 实际上是调用 seal
- 并且将现有属性的 writable: false
代码案例
// 阻止对象的拓展
Object.preventExtensions(obj);
obj.address = 12;
//密封对象 不能进行配置
Object.seal(obj);
delete obj.name;
// 冻结对象
Object.freeze(obj);
obj.name = "ske";
ES5&ES6 对象特性
ES5
对象和函数的原型
JS 中每一个对象都有一个特殊的内置属性,这个特殊的对象可以指向其他的对象
- 我们通过引用对象的属性 key 来获取一个 value 时,它会触发 Get 的操作
- 首先检查该对象是否有对应的属性,如果有的话就使用对象内的
- 如果对象中没有属性,那么会访问对象的
prototype
- 每一个对象都有一个原型属性
使用方式有两种:
- 通过对象的
_proto_
属性可以获取到(浏览器自己添加的,存在一定的兼容性问题) - 通过 Object.getPrototypeOf 方法可以获取
prototype 属性是函数特有的属性 我们的对象可以通过
Object.getPrototypeOf
或__proto__
来查看原型。
var obj = {};
function foo() {}
console.log(foo.prototype);
当我们这个对象有对多个共同值的时候,可以把相同的东西当如原型里,这样每次创建这个对象的时候,就可以直接调用而不是重新创建。
function Student(name, age) {
this.name = name;
this.age = age;
// 如果我们每个对象都创建那么这两个方法会出现很多的冗余
// this.running = function () {
// console.log(this.name + "running");
// }
// this.eating = function () {
// console.log(this.name + "eating");
// }
}
Student.prototype.running = function () {
console.log(this.name + "running");
};
Student.prototype.eating = function () {
console.log(this.name + "eating");
};
var stu1 = Student("jjj", 12);
var stu2 = Student("hhh", 18);
Constructor 属性
原型对象上面是有一个属性的:constructor,默认情况下原型都会有一个叫constructor
指向当前的对象
function Person() {}
var PersonProtype = Person.prototype;
console.log(PersonProtype);
console.log(PersonProtype.constructor);
console.log(PersonProtype.constructor == Person);
原型对象是可以重写的,当我们需要给原型添加更多的属性的时候一般我们会选择重写原型对象
我们也可以改变原型对象中 constructor 的指向的使用
//改变指向对象
Person.prototype = {
constructor: Person,
};
//修改枚举类型
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
});
这里要注意的是原生的 constructor 是不可枚举的,但是修改 constructor 的时候会让 constructor 的特性被设置为 true 这个时候需要修改一下对象默认属性设置
创建对象的内存表现:
如果我们向对象加入属性在之后的变化:
原型对象默认创建的时候,proto都是指向 object 的的 proto 的
多种继承方式
继承
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中)
- 多态:不同的对象在执行时表现出不同的形态
这里主要将 JS 中的继承,在了解继承之前我们需要了解 JS 中的原型链机制,这个是之后理解的关键
原型链
在 js 中我们不断的获取原型对象,原型链最顶层的原型对象就是 Object 的原型对象
[Object: null prototype] {}
这种提示一般有两个情况:
- 该对象有原型,且这个原型的属性指向 null 或者最顶层了
- 这个对象有很多的默认属性方法
ps:Object 是所有类的父类
我们也可以对原型链做一些自定义操作,比如这样:
var obj = {};
obj.__proto__ = {};
obj.__proto__.__proto__ = {};
obj.__proto__.__proto__.__proto__ = {
name: "小冷",
};
原型链实现继承
function Person() {
this.name = "l";
}
var p = new Person();
stu.prototype = p;
//name == l
stu.prototype.studying = function () {
console.log(this.name + "studying");
};
我们可以通过赋值原型的形式来实现继承,但是有一些弊端
- 直接打印对象是看不到属性的
- 这个属性会被多个对象共享,如果是引用类型就会造成问题
- 不能给父类传递参数,没法定制化
借用构造函数继承
为了解决原型链继承中存在的问题,constructor stealing
应运而生 ,借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数
- 因为函数可以任意调用
- 因此通过 apply 和 call 也可以再新创建的对象上实行构造函数
function Person(name, age, height, address) {
this.name = name;
this.age = age;
this.height = height;
this.address = address;
}
function Student(name, age, height, address, sno, score) {
Person.call(this, name, age, height, address);
this.sno = sno;
this.score = score;
}
可以使用父类的构造函数来实现创造,解决之前原型链的问题 在 ES6 之前一直是保持的这个方式,但是这个继承方式依然不是很完美
- 无论在什么情况下,都会调用两次父类构造函数。 一次是创建子类原型,一次是构造函数
- 所有的子类都会有两份父类的属性
继承最终方案
在继续的发展中, JSON 的创立者道格拉斯, 提到了新的继承方法,这也是目前 es5 阶段最合适的继承方案 寄生组合继承
- 结合原型类继承和工厂模式
- 创建一个封装继承过程的函数,在这个函数的内部来增强对象,最后将这个对象返回
function Person(name, age, height, address) {
this.name = name;
this.age = age;
this.height = height;
this.address = address;
}
Person.prototype.running = function () {
console.log(this.name + " running");
};
function Student(name, age, height, address, sno, score) {
Person.call(this, name, age, height, address);
this.sno = sno;
this.score = score;
}
// 原型继承
var obj = Object.create(Person.prototype);
console.log(obj.__proto__ === Person.prototype);
Student.prototype = obj;
// 上到真是环境 会封装用 为了兼容性可以多一个创造类的方法
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inherit(Subtype, Supertype) {
Subtype.prototype = object(Supertype.prototype);
// 需要构造方法
Object.defineProperty(Subtype, "constructor", {
enumerable: false,
configurable: this,
writable: true,
value: Subtype,
});
}
inherit(Student, Person);
Student.prototype.eating = function () {
console.log(this.name + "eating");
};
var stu = new Student("小明");
stu.eating();
对象方法补充
hasOwnProperty : 对象是否有某一个属于自己的属性
in/for in 操作符: 判断某个属性是否在对象或者对象的原型上
instanceof : 用于检测构造函数的原型,是否出现在某个实例对象的圆形脸上
isPrototypeOf:用于检测某个对象,是否出现在某个实例对象的原型链上
ES6
class 定义关键字
这个关键字主要是用于区别代码的编写方式的,之前编写中创建类和构造函数过于相似,而且代码并不容易理解
- 在 ES6 的标准中使用了 class 关键字来直接定义类
- 类本质上依然是前面所讲的构造函数、原型链的语法糖
- 比较少用 一般情况遇不到
使用方法
class Person {}
var Student = class {};
类的构造函数
在创建对象的时候给类传递一些参数
- 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的 constructor
- 当我们通过 new 操作符,操作一个类的时候会调用这个类的构造函数 constructor
- 构造函数时唯一的 不能出现多个
通过 new 关键字操作类的时候,会调用这个 constructor 函数
- 在内存中创建一个新的空对象
- 对象内部的[[prototype]]属性会被赋值为该类的 prototype 属性
- 构造函数内部的 this,会指向创建出来的新对象
- 执行构造函数的内部代码
- 如果构造函数没有返回非空对象,则返回创建出来的新对象
语法使用
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(this.name + "running");
}
eating() {
console.log(this.age + "eating");
}
}
var p1 = new Person("123", 123);
console.log(p1.name, p1.age);
class 创建的类和 function 直接创造构造方法创建的类时没有什么区别的
定义访问器
对象的属性描述符时有讲过对象可以添加 setter 和 getter 函数的,类同样也是可以的
var ojb = {
_name: "why",
set name(value) {
this._name = value;
},
get name() {
return this._name;
},
};
类的静态方法
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用 static 关键字来定义
class Person {
constructor(age) {
this.age = age;
}
static create() {
return new Person(Math.floor(Math.random() * 100));
}
}
extends 继承
ps : js 的继承属于只支持单继承
之前我们在 es5 中经过原型链继承,组合继承等等操作才解决继承的一些问题,但是在 ES6 中 他给我们提供了一个关键字 : extends
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(this.name + "running");
}
eating() {
console.log(this.age + "eating");
}
}
class Teacher extends Person {
constructor(name, age, title) {
// this.name = name
// this.age = age
super(title);
this.title = title;
}
Teaching() {
console.log(this.name + "Teaching");
}
}
class Student extends Person {
constructor(name, age, sno) {
// this.name = name
// this.age = age
super(name, age);
this.sno = sno;
}
studying() {
console.log(this.name + "studying");
}
}
var p1 = new Student("123", 123, "123");
console.log(p1.name, p1.age);
p1.eating();
我们只需要在类之后用 extends 指向需要被继承的类 就可以实现继承
Super 关键字
Class 为我们的方法中还提供了 super 关键字
- 执行 super.method(...) 来调用一个父类方法
- 执行 super(...) 来调用一个父类 constructor(只能在我们的 constructor 中)
- super 使用的位置有三个 子类构造函数,实例方法,静态方法
PS: 在子类的构造函数中使用 this 或者返回默认对象之前,必须先通过 super 调用父类的构造函数
在 JS 中 我们子类也是可以重写父类的方法的,但是当我们既想让子类有自己的操作,还想复用父类的实现,就可以使用 super 关键字
class Animal {
running() {
console.log("running");
}
}
class dog extends Animal {
running() {
console.log("dog four jio");
super.running();
}
}
var dog = new dog();
dog.running();
继承内置类
同样 我们可以继承内置类,比如数组 Array 类 加入我们想给数组加一些拓展比如获得第一位 获得最后一位 我们就可以继承数组对象添加我们想要定义的拓展
// 继承内置类
class CodeArray extends Array {
get lasItem() {
return this[this.length - 1];
}
get firstItem() {
return this[0];
}
}
var arr = new CodeArray(12, 14, 15, 16, 20);
console.log(arr);
console.log(arr.lasItem());
类的混入 mixin
JavaScript 的类只支持单继承,混入的思想可以帮助我们利用函数的方式实现嵌套继承,
- 它可以通过编写混入函数以返回新对象的方式实现多个继承
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
swimming() {
console.log(first);
}
}
function mixinRunner(BaseClass) {
return class extends BaseClass {
running() {
console.log(this.name + " running~");
}
};
}
function mixinEater(BaseClass) {
return class extends BaseClass {
eating() {
console.log(this.name + " eating~");
}
};
}
class NewPerson extends mixinEater(mixinRunner(Person)) {}
var NP = new NewPerson();
NP.swimming();
ES6 新的语法糖
ES6 中对 对象字面量 进行了增强,称之为 Enhanced object literals
主要包括:
- 属性的简写:Property Shorthand
- 方法的简写:Method Shorthand
- 计算属性名:Computed Property Names
属性的简写
当属性和变量名字完全一样的时候 可以将他省略 之后生成的代码还是和之前无差
/*
1. 属性的简写
*/
var name = "123";
var age = 12;
var obj = {
name,
age,
};
方法的简写
/*
1. 属性的增强
*/
var obj = {
running: function () {},
//简写为
running() {},
};
计算属性名
var address = "us";
var obj = {
[address]: "北京",
};
解构 Destructuring
ES6 中新增了一个从数组或对象中方便获取数据的方法,它是一种特殊语法
数组的解构:
- 基本解构过程
- 顺序解构
- 解构出数组:…语法
- 默认值: undefind
var names = ["nnn", "qqq", "lll", "fff"];
var [names1, names2, ...names] = names;
//输出结果 nnn,qqq,[lll,fff]
对象的解构:
- 基本解构过程
- 任意顺序
- 重命名
- 默认值
var obj = { name: "uuu", age: 12, height: 1.88 };
var { height, name } = obj;
//需要重命名
var { height: hei, name } = obj;
默认值;
var { name, age, grilfrineds: gf = "lucy" } = obj;
对象解构的应用:
function getPostion({ x, y }) {
var a = x + y;
}
getPostion({ x: 10, y: 20 });
实现函数 apply /call / bind
实现函数内容
首先我们要遵循封装的思想 , apply 和 call 方法 其实就是调用的方式不同而已
所以我们可以将这两个方法调用的共同点封装成一个函数 接下来只需要用不同的方式调用就可以了
function foo(name) {
console.log(this, name, age);
}
// foo.apply("aaa", ["hyc", 12])
// foo.call("aaa", "lebron", 38)
function execFn(thisArg, otherArgs, fn) {
// 1、 获取 thisArg 确保是一个对象类型
thisArg =
thisArg === null || thisArg === undefined ? window : Object(thisArg);
// thisArg 传入的第一参数是要绑定的this
Object.defineProperty(thisArg, "fn", {
enumerable: false,
configurable: true,
value: fn,
});
thisArg.fn(...otherArgs);
delete thisArg.fn;
}
Function.prototype.hycApply = function (thisArg, otherArgs) {
execFn(thisArg, otherArgs, this);
};
Function.prototype.hyccall = function (thisArg, ...otherArgs) {
execFn(thisArg, otherArgs, this);
};
foo.hycApply({ name: "hyc" }, ["james", 25]);
foo.hyccall(123, "why", 18);
Function.prototype.hycbind = function (thisArg, ...otherArgs) {
thisArg =
thisArg === null || thisArg === undefined ? window : Object(thisArg);
Object.defineProperty(thisArg, "fn", {
enumerable: false,
configurable: true,
writable: false,
value: this,
});
return (...newArgs) => {
// var allArgs = otherArgs.concat(newArgs)
var allArgs = [...otherArgs, ...newArgs];
thisArg.fn(...allArgs);
};
};
var newFoo = foo.hycbind("abc", "hyc", 30);
// newFoo(1.88,"广州")、
newFoo();
ES6-ES13
es6
JS 代码执行过程中需要了解的 ECMA 文档的术语
- 执行上下文栈:Execution Context Stack,用于执行上下文的栈结构;
- 执行上下文:Execution Context,代码在执行之前会先创建对应的执行上下文;
- 变量对象:Variable Object,上下文关联的 VO 对象,用于记录函数和变量声明;
- 全局对象:Global Object,全局执行上下文关联的 VO 对象;
- 激活对象:Activation Object,函数执行上下文关联的 VO 对象;
- 作用域链:scope chain,作用域链,用于关联指向上下文的变量查找;
let/const 基本使用
ES6 开始新增了两个关键字可以声明变量:let、const
- let、const 不允许重复声明变量
- let 不会作用域提升
- let、const 在执行声明代码前是不刻意访问的
var、let、const 的选择
- 在未来的开发中 很少会使用 var 来声明变量来开发了
- let const 比较推荐在开发中使用
- 推荐优先 使用 const 保证数据的安全性不会被随意的篡改
- 只有需要重复赋值的时候才使用 let
模板字符串
ES6 允许我们使用字符串模板来嵌入 JS 的变量或者表达式来进行拼接,使用 ``` `符号来编写字符串,称之为模板字符串
可以在模板字符串的时候用 ${}
来嵌入动态内容
``` ` 符号还可以调用方法自动传入参数
const name = "hyc";
const age = 18;
function foo(...args) {
console.log("111", args);
}
foo`my name is ${name},age is ${age},height is ${1.88}`;
函数默认值
ES6 之后 函数允许给参数一个默认值
function test(x = 10, y = 10)
可以使用这种方式来给函数的参数加入默认值,允许我们使用表达式比如
x = 1 || x > 1
Symbol 的基本使用
在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突
- 比如在新旧值同名的情况下混入 会有一个值被覆盖掉
- 比如之前有的 fn 属性 如果新添加一个 fn 属性 内部原有的 fn 会怎么样?
Symbol 就是为了解决上面的问题,用来生成一个独一无二的值。
- Symbol 值是通过 Symbol 函数来生成的,生成后可以作为属性名
- 在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol 值
const s1 = Symbol();
const s2 = Symbol();
const obj = {
[s1]: "aaa",
[s2]: "aaa",
};
// 获取 symbol 值 对应的key
console.log(Object.getOwnPropertySymbols(obj));
Set 的基本使用
在 ES6 之前,我们存储数据的结构主要有两种:数组、对象。
在 ES6 中新增了另外两种数据结构:Set、Map,以及它们的另外形式 WeakSet、WeakMap。Set 中数据是不能重复的
常用方法
set 支持 for of
Set 常见的属性:
- size:返回 Set 中元素的个数;
Set 常用的方法:
- add(value):添加某个元素,返回 Set 对象本身;
- delete(value):从 set 中删除和这个值相等的元素,返回 boolean 类型;
- has(value):判断 set 中是否存在某个元素,返回 boolean 类型;
- clear():清空 set 中所有的元素,没有返回值;
- forEach(callback, [, thisArg]):通过 forEach 遍历 set;
// 创建 set
const set1 = new Set();
set1.add(11);
set1.add(12);
set1.add(13);
set1.add(11);
console.log(set1.size);
WeakSet 使用
WeakSet 和 Set 有什么区别
- WeakSet 不可以遍历
- WeakSet 中只能存放对象类型,不能存放基本数据类型
- WeakSet 对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么 GC 可以对该对象进行回收;
let obj1 = { name: "why" };
let obj2 = { name: "hyc" };
const weakset = new WeakSet();
weakset.add(obj1);
weakset.add(obj2);
Map 基本使用
Map,用于存储映射关系
之前我们可以使用对象来存储映射关系,他们有什么区别
- 在之前的学习中对象存储映射关系只能用字符串(ES6 新增了 Symbol)作为属性名(key)
- 某些情况下我们可能希望通过其他类型作为 key,比如对象,这个时候会自动将对象转成字符串来作为 key;
这个时候就可以使用 map
const info = { name: "hyc" }
const map = new Map()
map.set(info, "aaaa")
Map 的常用方法
Map 常见的属性:
- size:返回 Map 中元素的个数;
Map 常见的方法:
- set(key, value):在 Map 中添加 key、value,并且返回整个 Map 对象;
- get(key):根据 key 获取 Map 中的 value;
- has(key):判断是否包括某一个 key,返回 Boolean 类型;
- delete(key):根据 key 删除一个键值对,返回 Boolean 类型;
- clear():清空所有的元素;
- forEach(callback, [, thisArg]):通过 forEach 遍历 Map;
WeakMap 的使用
WeakMap,也是以键值对的形式存在的。
WeakMap 和 map 的区别
- WeakMap 的 key 只能使用对象,不接受其他的类型作为 key;
- WeakMap 的 key 对对象想的引用是弱引用
- WeakMap 不能遍历
WeakMap 常见的方法有四个:
- set(key, value):在 Map 中添加 key、value,并且返回整个 Map 对象;
- get(key):根据 key 获取 Map 中的 value;
- has(key):判断是否包括某一个 key,返回 Boolean 类型;
- delete(key):根据 key 删除一个键值对,返回 Boolean 类型;
拓展知识
数值的表示
允许使用二进制 八进制来赋值
const num1 = 100;
const num2 = 0b100;
ES2021 新增数字过长可以用 _ 来连接
const num1 = 100_000_000;