# 你不知道的 JavaScript-读书分享
**Repository Path**: Lizhihang123/JavaScript_strength
## Basic Information
- **Project Name**: 你不知道的 JavaScript-读书分享
- **Description**: 这里记录了《你不知道的JavaScript》的读书分享,争取更完整个系列
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-11-26
- **Last Updated**: 2022-11-26
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 第一部分
## 1. 作用域
**作用域**:var a = 2; 这个变量a,存到哪里,我们要用到这个变量的时候,去哪里找他呢?需要一套**规则**,这套规则就是作用域。
### 1.1 编译原理
**JS是什么类型的语言?**
1. JS通常被归类为 动态语言或者是解释语言。但事实上,它是一门**编译语言**。又和传统的编译语言不同,JS不是提前编译的。 --存在疑惑
在《JS权威指南里面》把JS归类为`解释语言`
1. 动态语言 vs 静态语言
**静态语言(强类型语言)**,指程序在编译时,就要确定,变量的数据类型,运行的时候是不能修改的。
**动态语言(弱类型语言)**,指程序在运行时,确定变量的数据类型,过程中是可以修改的。
2. 解释语言 vs 编译语言
**编译语言**,指程序先编译成代码值,转化为机器语言,中间文件,在运行的时候,能够让计算机直接执行。
**解释语言**,在运行的过程中,才翻译程序为机器语言,那么都叫做解释语言。只要不是直接“翻译成机器指令并且直接运行机器指令”的编译语言,都是解释语言。
**区别:**编译语言语言把代码编译好,生成一个中间文件,后面可以直接机器执行。解释语言,执行的时候,进行翻译转化。解释语言,需要一个解释器。【`没有一个标准,都是概念,我们可以去进行讨论`】
JS:有执行前的性能优化,比如JIT(及时编译器),一个代码执行超过一次,就成为**warm**,JIT就会把这个代码送给编译器,进行保存,下一次执行时,直接跳过“翻译的过程”,这样性能就能够提升很多。后来才加入的
```js
for(i=0; i <= 1000; i++){
sum += i;
}
```
https://juejin.cn/post/6844903559536836616
2. 在传统的编译语言的流程中,程序中的一段源代码在执行前会经历三个步骤。统称为编译
1. **分词/词法分析**。var a = 2; -> var、a 、=、2
2. 解析/语法分析。将词法单元流,转化为一个 “元素逐级嵌套”所组成的代表程序语法的树。=> **抽象语法树**
https://astexplorer.net/
3. 代码生成。将AST转化为**可以执行的的代码**,让**机器**能够识别。
4. JS要更加**复杂**, 在语法分析和代码生成阶段有特定的步骤,对性能进行优化。**JS的编译通常发生在执行前的几微秒**,JS引擎用尽了各种办法,比如(JIT),来保持性能的最佳。
### 1.2 理解作用域
#### 1.2.1 演员表
1. 引擎
2. 编译器
3. 作用域
#### 1.2.2 对话
代码生成,第三步时,**var a = 2**是如何生成的
1. 遇到var a; 编译器会问,作用域,是否当前的作用域里面已经有和a同名的。
- 如果有,这个var a就会被忽略。
- 否则会要求作用域,在当前作用域的集合中**声明一个新的变量**
2. 接下来,编译器会为引擎生成运行时所需要的**代码**。这个代码能够处理a = 2的操作。
- **引擎**来啦~先问作用域,当前作用域集合里面是否有一个名字是a的变量。
- 如果有,引擎就使用这个变量。如果没有,引擎就继续去查找。
- 如果最终找到啦,**就把2赋值给a**,否则引擎就会举手示意,抛出错误。
#### 1.2.3 编译器有话说
编译器进行代码生成时,**引擎**要查找变量a判断是否已经用过时,需要作用域协助。
引擎是怎么找到变量的呢?
**LHS查询和RHS查询**:其实就是赋值操作的左侧和右侧
```js
function foo(a) {
console.log(a)
}
foo(2)
```
1. foo(a)我们需要找到a的值,是2,找到“找到赋值的目标”,是RHS
2. 2会给到a,有一个赋值操作,所以是LHS查询
3. 对console对象也有一个RHS查询,因为console本身也是一个对象,要找到这个**目标值**
4. console.log(a)找到a的值是什么,有一个RHS操作。如果console.log()函数原生实现里面也有一个赋值操作,那么就有一个LHS查询
5. var a = 2
#### 1.2.4 引擎和作用域的对话
```js
function foo(a) {
console.log(a)
}
foo(2)
```
1. 引擎 (我需要给foo进行**RHS**引用,你见过他吗) -> 作用域。函数声明是一个RHS查询,我要找到这个声明的函数值
2. 作用域 (别说,我还真的见过,编译器那小子刚刚声明而来他,是一个函数),给你 -> 引擎(不理解 函数是RHS查询)
3. 引擎 (哥们太够意思了,我么执行foo)
4. 应该还有一个RHS查询的 引擎 (哥们,我要为a进行**LHS**查询,这个你见过不)-> 作用域
5. 作用域 (这个我也见过,编译器刚刚把他声明为foo的一个形参了,拿去)
6. 引擎 (大恩不言谢,你总是这么棒,我要把2给到a) -> 作用域
7. 引擎 (哥们,不好意思,我要来麻烦你了,我要给console进行一个**RHS**查询,你见过不) -> 作用域
8. 作用域 (咱俩谁跟谁啊,再说我就是干这个的。这个我也有,**console是一个内置对象**,给你)-> 引擎
9. 引擎 (么么哒,我得看看这个里面是不是有一个log,太好了,找到了是一个函数)
10. 引擎 (哥们,能帮我再找一下对a的**RHS**引用吗,虽然我记得它,但想再确认一次)
11. 作用域 (放心吧,这个变量没有动过,拿走,不谢)
12. 引擎 (真棒,我来把a的值传递给log)
#### 1.2.5 小测验
```js
function foo(a) {
let b = a
return b + a
}
var c = foo(2)
```
LHS查询几个?
RHS查询有几个?
LHS查询有3个:a(形参)、b(把a给到b,找到b)、c(把函数的结果给到c,找到c)
RHS查询有4个:**函数的声明,应该是一个RHS查询。**2赋值给a(需要找到2这个值)、a赋值给b、查找b、查找a
1.先讲到作用域的查找,var a = 2;这样一句代码,他涉及两个步骤,第一个就是var a; a = 2;
2.var a 就是一个LHS查询 | a = 2就是一个RHS查询
3.编译器会去问作用域变量声明了没,没声明,编译器就声明它 + 引擎会和作用域配合,问变量声明好了嘛,声明好了话,我们就把2赋值给a吧
### 1.3 作用域嵌套
概念:当一个块或者函数作用域,嵌套在另一个块或者函数中时,就发生了作用域的嵌套。
如何查找:因此,在当前作用域中无法找到某个变量时,引擎就会去外层作用域查找,找到,全局没有,无论如何都会停止
```js
function foo(a) {
console.log(a + b)
}
var b = 2
foo(2)
```
**对话是什么样的呢?**
引擎:你见过b变量嘛,我要为其进行RHS查找。
作用域:见都没见过,走开
引擎:foo的上级作用域兄弟,咦?有眼不识泰山,原来你是全局作用域大哥,太好了。你见过b吗?我需要对它进行RHS引用。
作用域:当然了,给你吧

### 1.4 异常
**你知道 TypeError和ReferenceError两种错误的区别是什么嘛**
```js
function foo(a) {
console.log(a + b)
b = a
}
foo(2)
```
1. 因为找不到b赋值,所以RHS查询失败,报错**ReferenceError**,作用域查找失败
2. 如果是有 b = 2,尽管没有let b = 2,LHS查询失败,那么全局会自动创建一个b变量(非严格模式下面的)
3. 如果是对非函数类型的值进行函数调用,或者是null.a这样,就是**TypeError**(作用域判断能成功,找到变量,但是操作是不合法的)
### 1.5 小结
1. 编译原理:编译原理有三个步骤,先是分词(var a = 2 -> var、a、=、2),然后是转化为AST,最后是转化为机器可以执行的代码。JS事实上是编译语言,和传统的不太一样,编译时会做很多性能优化,一般编译完,代码就会立即的执行。
判断标准是什么:
- 只要有编译的过程存在,就是编译语言
- 只要不是生成一个全局的中间件文件,交给机器执行,就是解释语言。JS他是逐行解析代码,某段代码是warm了,才会交给JIT及时编译器去编译,下一次就不会重新解释这段代码。
2. 作用域:就是变量存到哪里,存好后续要用的时候,如何使用的问题
1. 作用域的整个规则,有引擎、编译器、作用域的参与
2. (var a = 2时)编译器在分词和转化为AST之后,会问作用域是否有a这个变量,有就忽略,没有就声明。接着是引擎来了,引擎会问作用域,a变量是否声明好了,当前作用域没有就会去上一层作用域查找。
3. LHS查询和RHS查询:
1. LHS查询,就是查找a变量这个来源;RHS查询就是查找值,var a = 2 先LHS再RHS
2. 这两个查询都在当前作用域先查询,如果查不到,就升级到上一层作用域。到全局没有就停止
4. 两种错误
1. TypeError是指类型错误,比如 let a = 1; a.name 就是类型错误
2. Reference,如果是没有声明变量,LHS查询错误,就是Reference Error
## 2. 词法作用域
### 2.0 词法阶段
**词法作用域:**你的代码写在哪里,词法作用域就在哪里。
```js
function foo(a) {
let b = a * 2
function bar (c) {
console.log(a, b, c)
}
bar (b * 3)
}
foo(2) // 2, 4, 12
```
1. 全局作用域,只有**foo**
2. foo里面的一层作用域,有a, bar, b
3. bar里面的作用域也有b,引擎就不需要冒到foo的作用域里面去查找了
4. 作用域的查找会在找到第一个标识符就停止。
5. 多层的嵌套的作用域,可以定义同名的变量,最近的会遮蔽最远的,就是**遮蔽效应**。
注意点:
1. **全局变量,自动成为全局对象的属性。**直接通过,window.a访问a,可以访问被遮蔽的变量。但是,非全局变量被遮蔽了,无论如何也访问不到。但是从ES6中,let和const和class声明的变量就与全局对象失去挂钩。
1. 没法在编译的时候就报出这个错误,只有在运行的时候才能知道。因为全局变量很可能是顶层对象的属性,对象的属性是动态的
2. 程序员很容易一不小心就创建全局变量
3. 顶层对象的属性到处可以读写,不利于模块化编程
4. window对象是有实体含义的,浏览器的窗口对象。**var和function的依旧还是**
```js
// let a = 123
var a = 123
function fn() {
console.log(window.a);
}
fn()
```
2. **函数的词法作用域,只由它被声明时所处的位置决定**。无论函数在哪里被调用,也无论它如何被调用。
```js
function foo() {
var a = 10
function baz() {
console.log(a)
console.log('baz')
}
bar(baz)
}
function bar(fn) {
fn() // 闭包函数 作用域还是在上面
}
foo()
```
### 2.1 查找
**引擎如何查找**
1.作用域之间的结构和位置关系是**信息**,引擎依赖这个**信息**,去查找标识符的位置
2.console.log()查找a、b、c三个变量的引用。先从最内部,bar的气泡开始查找,再到foo的气泡里面找,找到了,就停止。
3.遮蔽效应,如果最近的一个作用域找到了变量,就停止不会去访问上一级的。但是可以通过window.a的方式(仅限于var 关键字声明的变量)
### 2.2 欺骗词法
#### 2.2.1 eval
1. 可以接受一个字符串作为参数,执行字符串里面的代码,不仅仅是字符串
2. 修改当前词法作用域的内容,覆盖上一层作用域的b
3. 对性能带来的损耗很大,不太能够接受
```js
// let不行 var可以
function foo(str, a) {
eval(str) // 原本作用域只有1,但是现在多了b,var b = 3也被执行了
console.log(a, b);
}
var b = 2
foo('var b = 3', 1)
```
声明函数也可以
```js
function foo2(fn) {
eval(fn)
console.log(sing);
}
foo2('function sing() {}')
```
严格模式并不能行
```js
function foo3(str, a) {
"use strict"
eval(str) // 原本作用域只有1,但是现在多了b,var b = 3也被执行了
console.log(a, c);
}
foo3('var c = 3', 1)
```
1.setTimeout和setInterval第一个参数也可以是字符串,但是千万不要使用它们,过时了
2.new Function('', '')第二个参数也可以写一些函数体,尽量不要使用
```js
// setTimeout--------------------------------
setTimeout('console.log(100)', 1000) // 100
// new Function--------------------------------
let foo4 = new Function('a,b', 'console.log(`new Function`)')
console.log(foo4);
```
#### 2.2.2 with
1. 比起一个一个的修改obj,我们直接用with就能够不用频繁的修改
```js
var obj = {
a: 1,
b: 2,
c: 3
}
// obj.a = 2
// obj.b = 3
// obj.c = 4
with (obj) {
a = 3
b = 4
c = 5
}
console.log(obj); // 成功修改
```
```js
function foo(obj) {
with (obj) {
a = 2
}
}
var o1 = {
a: 3
}
var o2 = {
b: 3
}
foo(o1)
console.log(o1.a); // 2
foo(o2)
console.log(o2.a); // undefined
console.log(a); // 2 发生了变量泄露到了全局作用域
```
1. o1传进去,找到o1作用域里面的属性a,能够进行修改
2. o2传进去,o2里面没有属性a,无法进行修改
3. 同时,非严格模式,o2.a访问不到a属性,就会去全局作用域,隐式的创建a变量,就会有内存泄露
小结:
1. eval是直接修改 **自己所处的作用域**
2. with是修改**传进来的变量,里面的作用域**,能够影响里面的作用域的内容,和他进行交互。换种说法,传进来一个对象时,那么就包含了对这个对象的“作用域”里面的引用,就可以修改这个作用域里面的内容
3. 都不建议使用
#### 2.2.3 性能
1.JS引擎会在编译阶段进行很多的性能优化。一些优化,依赖代码的词法进行静态分析。(如果词法作用域在优化时已经确定好了,更有利于JS引擎的分析,找到变量速度更快)
2.发现eval/with,引擎只会简单假设关于标识符的判断都是`无效的`,因为词法分析时,无法明确知道eval里面会接受到什么,会怎么修改
3.所有优化可能都是无意义的。不要使用eval和with
### 2.3 小结
1.**词法作用域**:在变量定义时,就已经确定好了。不管函数在哪里调用
2.**词法作用域查找**:依据,变量之间的结构和位置关系(是标识符),去查找变量。先从最里面的变量开始查找,往外面找
3.**欺骗词法**:**eval可以修改当前作用域的内容**,with可以修改传入对象的作用域的内容。这两个都会破坏标识符的查找机制,让引擎变得笨笨的,降低性能。索性不要使用
## 3. 函数作用域和块作用域
1. 作用域里面包含了一系列的气泡,每一个都可以作为**容器**,**容器里面**包含了标识符(**变量,函数**)的定义,这些气泡相互嵌套。
2. 什么生成了气泡,只有函数会生成气泡嘛?其实不是这样
### 3.1 函数中的作用域
```js
function foo(a) {
var b = 2
function bar () {
}
var c = 3
}
bar() // 失败
console.log(a, b, c) //失败
```
1. 全局作用域的气泡包含foo,foo作用域气泡包含,a/b/bar/c, bar作用域气泡又会有自己的内容
2. 无法从外部(全局)访问a b bar c;**但是foo里面和bar里面都是可以访问**
3. **函数作用域:**属于这个函数的全部变量都可以在整个函数的范围里面被使用,包括嵌套作用域 => 这个设计很有用,JS的动态特性,能够需要改变变量的类型
```js
function foo(a) {
var b = 2
function bar () {
// 又重新改为false
b = false
}
var c = 3
}
bar() // 失败
console.log(a, b, c) //失败
```
### 3.2 隐藏内部实现
```js
function doSomething(a) {
b = a + doSomethingElse(a * 2)
console.log(b * 3)
}
function doSomethingElse(a) {
return a - 1
}
var b
doSomething(2)
```
1. b和doSomethingElse都是为了给doSomething使用的
2. 但是b和doSomethingElse都放在全局,可能全局的其它地方都有意无意的使用它们两个,就不好。更好的做法如下面所示,全局不能够轻易的访问`b`和`doSomethingElse`
```js
function doSomething(a) {
b = a + doSomethingElse(a * 2)
console.log(b * 3)
function doSomethingElse(a) {
return a - 1
}
var b
doSomething(2)
}
```
1. **隐藏内部实现**:一般对于函数的思路是,声明一个function fn() {}, 然后写代码;但是也可以反过来,就是把程序的一段代码,塞到一个函数里面去,藏起来,这样只有这个里面的作用域,或者里面的嵌套作用域,能够访问里面的变量。
2. **最小特权原则**:最小限度的暴露必要的内容,而将其它内容都隐藏起来,模块、API的设计。 => 延伸到如何选择作用域来包含变量和函数。变量和函数都在全局,那么就会破坏这个原则。
3. **减少命名冲突**: 假设全局下面,也要设置一个函数`doSomethingElse`,这个函数的操作比如是return a - 2,现在就能够使用了。我们有`遮蔽效应`.
**全局引入包**,比如import axios from 'axios', axios.instance为什么是要以对象的形式访问呢?假设以另一个包里面也是有instance变量,没有锁在一个对象里面,岂不就是冲突
**vuex里面的模块机制**,多个模块,变量名字一样也没关系。访问变量state的时候,store.state.a.name,变量注入到了一个特定的区域
### 3.3 函数作用域
```js
var a = 2
function foo() {
var a = 3
}
foo()
console.log(a) // 2
```
1. foo函数能够包装 隐藏一个变量a
2. 但是foo函数会污染全局的作用域
3. 并且foo()手动调用,多了一行代码。如果foo不污染全局,并且能够自动而不是手动调用,就更好
```js
var a = 2
(function foo() {
var a = 3
})()
console.log(a) // 2
```
1. (function foo() {})()里面的function不是一个函数声明,而是一个函数表达式。
**区别**:第一个字是function还是其它。
2. 函数会自己执行
3. foo变量在全局是访问不到的,在()里面被包裹住了
#### 3.3.1 具名和匿名
>函数到底是有名字的好,还是没有名字的好
```js
setTimeout(function() {
})
```
1. 函数表达式没有函数名,就是匿名函数表达式
2. 没有函数名的缺陷:
1. 在栈追踪中,不会显示出有意义的名字,调试会很困难
2. 如果函数在递归中,要使用自身时,也会变得有限制。(不理解)
3. 代码变得不那么可读了
3. `函数表达式有名字是最佳实践`
```js
setTimeout(function timeoutHandler() {
})
```
#### 3.3.2 立即执行函数表达式
1. 立即执行函数表达式,也叫**IIFE(Immediately Invoked Function Expression)**
能够创建一块作用域
```js
var a = 2
(function ()) {
var a = 3
})()
console.log(a) // 2
```
2. 添加函数名也是一个值得推广的实践
```js
var a = 2
(function foo() {
var a = 3
})()
console.log(a) // 2
```
3. 传递参数进去,global可以取任意你想取的名字
```js
var a = 2
(function foo(global) {
var a = 3
console.log(a)
console.log(global)
})(window)
console.log(a)
```
4. 另一种冗长的方式,但是有的朋友认为他容易理解
1. 打印global的函数,被作为参数,传递到第一个函数里面去,然后执行,输出window
```js
(function (def) {
def(window)
})(
function(global) {
console.log(global)
}
)
```
### 3.4 块作用域
查看下面的两段代码,都不是块作用域的体现
```js
// 1. 这段代码里面的i会泄露到全局
for (var i = 0; i < 10; i++) {
console.log(i);
}
console.log(i);
// 2. 下面代码的bar也是会泄露到全局的
let foo = true
if (foo) {
var bar = foo * 2
bar = something(bar)
console.log(bar);
}
function something(a) {
return a * 2
}
console.log(bar);
```
让变量在一个作用域里面才能生效,不会被混乱的使用,是能够提升代码的可维护性的
#### 3.4.1 with
我们曾经学习过的with关键字,里面就是创建了一块单独的作用域,外部是不能使用到a、b、c
```js
var obj = {
a: 1,
b: 2,
c: 3
}
// obj.a = 2
// obj.b = 3
// obj.c = 4
with (obj) {
a = 3
b = 4
c = 5
}
console.log(obj); // 成功修改
```
#### 3.4.2 try/catch
catch里面的变量,也会和{}里面的变量进行绑定
```js
try {
undefined()
} catch(err) {
console.log(err) // 打印的err 只在 catch这个作用域里面是有效的
}
console.log(err) // ReferenceError
```
3.4.3 小结
1. 在ES3的时候就是有块级作用域了,就是with和try / catch语法
2. 后来我们才会有let关键字
```js
{
let a = 10
console.log(a)
}
console.log(a) // Reference Error
```
```js
try {
throw 2
} catch(err) {
console.log(err)
}
console.log(err)
```
3. 我们写的ES6代码,如何在ES6之前的环境里面运行呢。就是在代码运行前,进行构建,需要工具来帮我们进行构建。我们可以写的爽,享受块级作用域。
- 谷歌的一个项目,Traceur,就是将ES6代码转化为ES6之前的环境,大部分是ES5
- 这个项目会把代码转化成什么样子?let -> catch
- try / catch的从ES3就开始存在了
#### 3.4.3 let
我们来观察一段代码
```js
let foo = true
if (foo) {
let bar = foo * 2
bar = something(bar)
console.log(bar)
}
function something(a) {
return a * 2
}
console.log(bar)
```
1. 上面的bar变量,只能在if语句里面进行访问
2. 这是隐式的使用let,让变量和这个作用域进行绑定,我们马上就会介绍一个显示的
3. 隐式的绑定,不利于开发
我们看下面的一个代码
```js
var foo = true
if (foo) {
{ <-- 这个就是一个显示的块
let bar = foo * 2
bar = something(bar)
console.log(bar)
}
}
function something(a) {
return a * 2
}
```
1. 正是因为有了这个花括号的存在,我们称他为显示的创建一个块级作用域
2. 只要声明有效,在声明中的任意位置都是可以使用{..}来进行创建的
3. 如果要重构代码,直接移动这个{}就好了
```js
{
console.log(bar) // ReferenceError
let bar = 2
}
```
1. 上面代码,先打印bar是报错,因为let声明的变量,是不会有变量的提前声明的,var是会有的
```js
function process(data) {
// 搞点事情
}
{
let someReallyBigData = {...}
process(someReallyBigData)
}
var btn = document.getElementById('my_button')
btn.addEventListener('click', function() {
....
})
```
1. 我们发现,因为{}的存在。someReallyBigData变量是let声明的,在process函数执行完毕后,因为btn的点击事件并不会使用到someReallyBigData这个变量,那么这个变量就会被垃圾回收机制给回收掉
2. 如果是没有那层{}怎么办呢?那么这个变量就不会被垃圾回收,可能会一直占用着内存
我们来看一段很常见的代码
```js
for (let i = 0; i < 10; i++) {
console.log(i)
}
console.log(i) // ReferenceError
```
1. 正是因为使用了let,全局就访问不到i。如果不是let而是var呢,就会有全局变量污染
```js
{
let j;
for (let j = 0; j < 10; j++) {
let i = j
console.log(i)
}
}
```
1. 上面的循环中,每一次迭代,都会重新进行绑定,每次绑定时,j都是新的值,而不是始终10
```js
var foo = true,
bar = 10
if (foo) {
var tub = 20
if (tub > bar) {
// 执行一些操作
}
}
// ---如果进行重构
```
var可以直接进行很轻松的移动,上面在if里面声明的变量就相当于是全局的。
但是let呢,不行
```js
var foo = true,
bar = 10
if (foo) {
// let tub = 20
}
let tub = 20 // let变量也要移动过来
if (tub > bar) {
// 执行一些操作
}
```
#### 3.4.4 const
1. const声明的必须是一个常量;初始值;没有变量提升,暂时性死区
2. 实质:保存的是地址。简单(值本身);复杂(指针),指针指向的区域不变。但是该区域里面的值,不能保证
```js
const obj = {name: 123}
obj.age = 45
obj = {height: 111} // 是错的
```
#### 小结:
1. 我们讲了with和try / catch是最早的块级作用域,不兼容的情况时,我们需要一些工具,把我们写的ES6代码转化为ES5或者更年轻的,其就是 try / catch
2. 我们讲了let变量
1. 存在块级作用域。并且有显示和隐式两种绑定方式。显示就是直接套一个{},这个对于以后的代码重构是很好的。
2. 我们讲了let的显示绑定作用域,垃圾回收机制也有关系,显示绑定能够明确告诉引擎,这段代码执行完毕,就回收
3. 我们讲了const变量,的本质。很多和let一致,还有常量的地方要尤其注意。
### 3.5 大结
1. 我们讲了函数作用域,一个函数里面的变量,在这个范围内都可以被使用,包括嵌套作用域,这个是一个很好的设计
2. 我们讲了隐藏内部实现,就是可以把一段代码隐藏到函数里面,这样不仅可以避免变量的命名冲突;还符合最小特权原则,把必要的代码尽量封装到函数里面
3. 我们分析了具名和匿名的两种情况,具名更有利于引擎的查找。而IIFE,立即执行函数,也最好是具名的形式。立即执行函数也是创建了一块独立的作用域
4. 块级作用域,有早期的with和catch的实践,还有let和const的新的实现。以及显示的声明变量,对于代码维护的好处。
## 4. 提升
### 4.1 先有鸡还是先有蛋
查看下面的代码
```js
a = 2
var a
console.log(a)
```
1.输出是2
2.变量不声明,直接赋值,可以这样使用,但是我们不推荐这样去做
查看下面的代码
```js
console.log(a)
var a = 2
```
1.输出的是undefined
2.变量提升会被提前,但是赋值并不会被提前
### 4.2 编译器再度来袭
1. JS引擎解释代码之前,先对代码进行编译,其中一部分工作,就是找到所有的声明。(嘿作用域哥们,你见过变量a嘛?如果见过,就会把这个作用域和变量a关联起来)。等执行完这一步,在处理下一步
2. 正是因为查找变量声明,所以,变量和函数的声明会在代码执行前,先被处理
3. var a = 2; -> var a、a = 2; 第一个变量声明是在编译阶段运行的,第二个变量声明是在原地等待着被执行。(只有声明本身会被提升,赋值等其它操作都会被留在原地)
```js
var a
a = 2
console.log(a)
---
var a
console.log(a)
a = 2
```
4. 变量声明的概念就是:变量和函数声明从它们在代码中出现的位置,被移动到了最上面,这个过程就是提升。
```js
foo()
function foo() {
console.log(a) // undefined
var a = 2
}
----
注意,var a只会提升到foo作用域的顶部
```
5. 函数提升,但是函数表达式不会提升.
```js
foo2() // foo2 is not defined TypeError
var foo2 = function bar() {
...
}
---
var foo2
foo2()
foo2 = function bar() {}
```
6. 即使是具名的函数表达式,也无法使用
```js
foo2() // foo2 is not defined TypeError
bar() // ReferenceError
var foo2 = function bar() {
...
}
```
### 4.3 函数优先
```js
foo()
var foo
function foo() {
console.log(1)
}
foo = function () {
console.log(2)
}
```
是1
如下:
1. foo函数声明优于变量声明
2. 重复的变量声明会被忽略
```js
function foo() {
console.log(1)
}
foo()
var foo //这一个会被忽略
foo = function () {
console.log(2)
}
```
```js
foo()
function foo() {
console.log(1)
}
var foo = function() {
console.log(2)
}
function foo() {
console.log(2)
}
```
是3。如上:
1. 重复的声明会被忽略
非常奇怪的声明,不要这么去做,不要在块级作用域汇总做这件事情。
```js
foo() // 不是一个函数
var a = true
if (a) {
function foo() {
console.log('a');
}
} else {
function foo() {
console.log('b')
}
}
```
小结:
1.函数和变量在一起,函数声明优先
2.函数名和变量名重复了,就会先声明函数,变量的会被忽略
3.不要在普通块作用域里面去声明函数
## 5. 闭包
### 5.2 闭包实质
闭包的概念:
1. 当函数可以记住并且访问它所在的词法作用域时,就产生了闭包。
2. 即使函数是在当前词法作用域外执行的
```js
function foo() {
let a = 2
function bar() {
console.log(a) // 2
}
bar()
}
```
上面代码,严格意义上来说不是闭包,只是应用了词法作用域的概念,而这个概念是闭包的很重要的一部分,而已。
```js
function foo() {
let a = 2
function bar() {
console.log(a)
}
return bar
}
let baz = foo()
baz()
```
上面是真正的闭包
1.foo函数执行返回函数bar
2.bar交给了baz变量,baz函数执行
3.能够访问到foo作用域的变量,即便bar函数在外部被调用
4.foo函数作用域的变量没有立即被垃圾回收机制给回收掉
无论以何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用
```js
function foo() {
var a = 2
function baz() {
console.log(a)
}
bar(baz)
}
function bar(fn) {
fn()
}
foo() // 快看,这也是闭包
```
```js
var fn
function foo() {
let a = 2
function baz() {
console.log(a)
}
fn = baz
}
function bar() {
fn() // 2
}
foo() // 调用 那么fn就有值了 fn的值是baz
bar() // bar调用 那么就是baz调用 就是输出a
```
### 5.3 现在我懂了
```js
function wait(message) {
setTimeout(function timer() {
console.log(message)
}, 1000)
}
wait('Hello, closure')
```
1. timer函数交给setTimeout执行
2. timer函数的作用域覆盖wait函数的作用域
3. wait函数执行时,1000ms后,内部的作用域不会消失,timer依然保持着对wait函数作用域的引用。
4. timer交给定时器,引擎会执行timer函数,内部作用域不会立即被回收,那么就是闭包
5. 定时器,事件监听器,ajax,跨窗口通信,只要使用了回调函数,就是在使用闭包
### 5.4 循环和闭包
```js
for (var i = 0; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000)
}
```
1. 我们预期输出0, 1, 2, 3, 4, 5。但是输出的是6个6,这是为什么呢?
2. 因为:延迟函数的回调是在循环接受才执行的
3. 到底有什么缺陷:我们假设每个函数是在每次循环中被创建的声明的,看起来好像捕获的i是不同的。但是实际上,它们捕获到的i是在一个作用域里面。封闭在一个共享的全局作用域里面。(var的for循环,会有变量泄露的问题)
**如何解决这个问题呢?**
1. 使用IIFE解决闭包
```js
for (var i = 0; i <= 5; i++) {
(function() {
setTimeout(function () {
console.log(i);
}, i * 1000)
})()a
}
```
这样能够解决嘛?这样不能够解决。因为封闭了一个作用域,但是里面是空的,还是会往上去查找
2. 进一步呢?
```diff
for (var i = 0; i <= 5; i++) {
(function() {
+ var j = i
setTimeout(function () {
console.log(j);
}, j * 1000)
})()
}
```
这样是能够解决的。var j = i 不是异步执行的。每次循环时,都会在所在作用域里面保存一个i。那么回调执行时,是能够访问到的。
3. 我们可以把变量传递进去,给到形参
```diff
for (var i = 0; i <= 5; i++) {
(function(j) {
setTimeout(function () {
console.log(j);
}, j * 1000)
})(i) // 我们把i传递进去,给到j,每次循环,传递的i都是各自的。然后IIFE每次封闭的作用域里面的形参也是各自的,这样定时器就能够访问到了
}
```
4. 我们可以使用块级作用域 关键字let
```js
for (var i = 0; i <= 5; i++) {
let j = i // 每一次都会重新声明一个新的j,来给定时器使用
setTimeout(function() {
console.log(j)
}, j * 1000)
}
```
5. 直接用let 声明i
```js
for (let i = 0; i <= 5; i++) {
setTimeout(function() {
console.log(j)
}, j * 1000)
}
```
小结:
1. 在for循环中,var关键字行不通,一方面是setTimeout里面访问的i是定时器结束时的i,是引用了全局作用域的i
2. 使用IIFE,并且传递进去变量,就能够解决这个问题,独立封闭了作用域
3. 使用let关键字能够创建块级作用域 能够解决这个问题
### 5.5 模块
1. 返回的对象,对象的属性值是函数,对函数的引用,-> 称得上模块
2. 返回的对象 引用的是里面的函数 而不是变量,里面的变量值是私有的状态,这是模块的常见操作
3. 创建模块的条件
1. 一定要调用函数调用它,才能创建闭包
2. 一定是至少返回一个封闭函数。如果返回的是对变量的引用,就不是闭包
```js
function coolModule() {
var something = 'cool'
var another = [1, 2, 3]
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join('!'))
}
return {
doSomething: doSomething,
doAnother: doAnother
}
}
let foo = coolModule()
foo.doAnother()
foo.doSomething()
```
使用IIFE 自调用函数 返回给foo变量,那么foo变量就是返回的对象,存着对内部函数的调用
```js
let foo = (function coolModule() {
var something = 'cool'
var another = [1, 2, 3]
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join('!'))
}
return {
doSomething: doSomething,
doAnother: doAnother
}
})()
foo.doAnother()
foo.doSomething()
```
**给模块去传递参数**
```js
function coolModule(id) {
function identify() {
console.log(id);
}
return {
identify: identify
}
}
let foo1 = coolModule('cool Module1')
foo1.identify()
let foo2 = coolModule('cool Module2')
foo2.identify()
```
**给模块去修改修改API的名字**
1. 在外部,如何修改publicAPI的值的内容呢?不是直接修改publicAPI.indentify1的内容,而是要调用changeAPI才能够修改
```js
function coolModule() {
let name = 123
let age = 456
let publicAPI = {
identify1: identify1,
change: change
}
function change() {
publicAPI.identify1 = identify2
}
function identify1() {
console.log(name);
}
function identify2() {
console.log(age);
}
return publicAPI
}
let foo = coolModule()
foo.identify1() // 123
foo.change()
foo.identify1() // 456
```
#### 5.5.1 现代的模块机制
模块工具
```js
let MyModules = (function () {
let modules = {}
function define(name, deps, impl) {
debugger
for (let i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]]
}
modules[name] = impl.apply(impl, deps)
}
function get(name) {
return modules[name]
}
return {
define: define,
get: get
}
})()
```
bar定义
```js
MyModules.define('bar', [], function () {
function Hello(who) {
return 'let me introduce you: ' + who
}
return {
Hello: Hello
}
})
```
foo定义
```js
MyModules.define('foo', ['bar'], function (bar) {
let she = 'xiaohong'
function awesome() {
console.log(bar.Hello(she));
}
return {
awesome: awesome
}
})
```
执行函数
```js
// 先执行它
let bar = MyModules.get('bar')
// 再执行它
let foo = MyModules.get('foo')
console.log(bar.Hello('xiaobai')); // let me introduce you: xiaobai
foo.awesome() // let me introduce you: xiaohong
```
小结:满足下面的两个条件的就是模块
1. 模块函数返回一个对象,对象包含对内层的函数的引用
2. 模块函数必须被调用
3. 不管变成了模块工具与否 都是函数
#### 5.5.2 未来的模块机制
bar.js
```js
function Hello(who) {
return 'Let me introduce ' + who
}
export default Hello
```
foo.js
1. 使用了Hello模块的里面的变量
```js
import Hello from './bar.js'
let hungry = 'hippo'
function awesome() {
console.log(Hello(hungry).toUpperCase())
}
export default awesome
```
baz.js
1.使用了Hello和awesome里面的变量
```js
import Hello from './bar.js'
import awesome from './foo.js'
console.log(Hello('123456'))
awesome()
```
#### 5.6 小结
1. 函数能够记住并且访问自己`定义时所在的词法作用域`,即便函数作为参数被传递到别的地方。
2. 模块的特征:包装函数,被执行; 包装函数返回一个对象,包含 函数的引用
## 附录A 动态作用域
1. 观察下面的代码 当bar执行时,我们去打印a,输出的是?
```js
function foo() {
console.log(a)
}
function bar() {
let a = 3
foo()
}
let a = 2
bar()
```
- 是3,因为JS是词法作用域,基于词法作用域foo函数执行内部没有时,就会去上一级的全局作用域查找。
- 如果是动态作用域,那么输出的应该是3,不是2了。动态作用域不关心,变量是如何声明以及是在哪里声明的,他们关心的是从何处调用的。**动态作用域链是基于调用栈,而不是作用域的嵌套。**
- 动态作用域是this的机制的表亲,动态作用域基于的也是,运行的时候确定的代码
## 附录B 块作用域的替代方案
1. ES3的时候就有一个块级作用域, try / catch 但是代码比较丑陋
2. 有工具,可以将ES6的代码转换成能在ES6之前环境中运行的形式
```js
try {
throw 2
} catch(err) {
console.log(err)
}
console.log(err) // VM61:6 Uncaught ReferenceError: err is not defined
```
3. Google有一个维护的项目叫Traceur,将ES6-> 转化为之前版本的可以兼容的代码
```js
{
try {
throw undefined
} catch(err) {
a = 2
console.log(a)
}
}
console.log(a)
```

4. 显式和隐式的创建作用域
# 第二部分 this和对象原型
## 第一章 关于this
前言:
1. this是JS当中很重要很复杂的机制
### 1.1 为什么要使用this
下面这一段,能够隐式的使用me和you对象里面的变量
```js
function identify() {
return this.name.toUpperCase()
}
function speak() {
let greeting = "Hello, I'm " + identify.call(this)
console.log(greeting);
}
let me = {
name: 'I am me'
}
let you = {
name: 'I am you'
}
```
下面这一段,必须要显示的传递变量和对象。而隐式的传递,使用上下文的对象,是非常重要的。(后续的重要性就可以体现出来)
```js
function identify(context) {
return context.name.toUpperCase()
}
function speak(context) {
let greeting = "Hello, I'm " + context.call(this)
console.log(greeting);
}
let me = {
name: 'I am me'
}
let you = {
name: 'I am you'
}
console.log(identify(me)); // I AM ME
console.log(identify(you)); // I AM YOU
speak.call(me) // Hello, I'm I AM ME
speak.call(you) // Hello, I'm I AM YOU
```
### 1.2 关于this的一些误解
我们来看下面这段代码,会输出什么?
```diff
function foo(num) {
console.log('foo: ' + num);
this.count++
}
foo.count = 0
for (let i = 0; i < 10; i++) {
if (i > 5) {
foo(i)
}
}
/*
foo: 6
foo: 7
foo: 8
foo: 9
*/
console.log(foo.count); // 0
```
1. 为什么foo.count输出的是0呢?首先,this指向函数自身是一个错误的理解。如果this指向函数自身,会认为this.count修改的就是foo函数身上的属性,但是事实并非如此。
2. 真相是,编译器会在全局下面,创建了一个全局的变量,count变量,此时它的值是NaN,NaN+1结果肯定还是1
3. 有时候我们会去回避上面的问题,用另一种方式来解决这个问题。给data对象上面挂载了这个属性,这样确实解决了this指向的问题,但这样并不能真正去理解this的工作原理。
- 因为这种用法使用的原理是词法作用域而不是this原理
```js
function foo(num) {
console.log('foo:' + num)
data.count++
}
let data = {
count: 0
}
for (let i = 0; i < 10; i++) {
if (i > 5) {
foo(i)
}
}
```
4.
```js
function foo(num) {
console.log('foo: ' + num);
foo.count++
}
foo.count = 0
for (let i = 0; i < 10; i++) {
if (i > 5) {
foo(i)
}
}
/*
foo: 6
foo: 7
foo: 8
foo: 9
*/
console.log(foo.count); // 4
```
如果是定时器就不行,原因是,里面是匿名函数。具名函数可以通过函数名直接访问自身,但是匿名函数不可以. => 同样回避了问题
```js
setTimeout(function () {
// .....
})
```
使用.call方法就没有回避问题,因为.call方法会修改this指向
```diff
function foo(num) {
console.log('foo: ' + num);
foo.count++
}
foo.count = 0
for (let i = 0; i < 10; i++) {
if (i > 5) {
+ foo.call(foo, i)
}
}
/*
foo: 6
foo: 7
foo: 8
foo: 9
*/
console.log(foo.count); // 4
```
## 第二章 this全面解析
### 2.1 调用位置
调用栈就是函数的一个调用链
容易出错和麻烦
```js
function baz() {
// baz
console.log('baz');
bar()
}
function bar() {
// baz --> bar
console.log('bar');
foo()
}
function foo() {
// baz --> bar --> foo
console.log('foo');
}
baz()
```
### 2.2 绑定规则
#### 2.2.1 默认绑定
```js
function foo() {
console.log(this.a);
}
var a = 3
foo() // 3
```
- 如果函数调用 前面没有任何的修饰符,那么就是默认绑定,会绑定给window
- 如果是严格模式,this就会绑定到`undefined`
- 虽然this的绑定规则完全取决于调用位置,但是foo一定要运行在非严格模式下才行。
- 也存在一种情况,foo在严格模式下面。但是this打印不是严格模式,这种也没有问题。
```js
function foo() {
console.log(this.a);
}
var a = 3;
(function () {
'use strict'
foo() // 3
})()
```
注意:非严格模式和严格模式不要混用。但是有可能你用的第三方库和你的代码的`严格程度`会有所不同,一定注意兼容性的细节。
###
### 3.1 对象的语法
**如何创建一个对象**
```js
// 字面量(文字语法)
let obj = {
name: 'li',
age: 20
}
// 构造函数形式的创建
let obj2 = new Object()
obj2.name = 'zhi'
obj2.age = 'hang'
```
>两种方式的区别?
>
>第二种创建对象的方式,必须一个一个添加对象的属性;
>
>第一个更加方便,一次性添加多个
### 3.2 JS有哪些基本数据类型
```js
number
string
boolean
null
undefined
(bigInt)
(symbol)
```
**null是一个bug,为什么?**
是语言的一个bug,虽然typeof null是object 但是它是基本数据类型。
**为什么typeof null 是object呢?**
在JS中,不同的对象在底层,都表示为二进制。在JS中,**二进制前三位为0**的都会被typeof 判定为object。null的二进制所有都是0,所以他的typeof的结果也是object
`以上本身并不是对象,而是基本数据类型.JS中万物皆对象的说法是错误的`
### 3.2 object及其子类型
object是复杂数据类型,对象、数组Array、正则RegExp、日期Date、错误Error
JS中还有很多内置对象
```js
String
Number
Boolean
Object
Function
Array
Date
RegExp
Error
```
**String、Number、Boolean这三个和基本数据类型一致不?**
他们看起来和基本数据类型一致,但是其实不一致,实现起来更加的复杂,他们是构造函数。
```js
let str = 'I am a string'
console.log(typeof str); // string
console.log(str instanceof String); // false
let strObject = new String('I am a stringObject')
console.log(typeof strObject); // object 因为strObject是由String构造函数创建的对象
console.log(strObject instanceof String); // true
console.log(Object.prototype.toString.call(strObject)); // [object String]
```
**str访问索引和length是如何访问的?**
I am a string 并不是一个对象 而是一个字面量 -> 如果要在字面量上进行 str[0]访问,str.length -> 需要转化为String对象 -> 但是JS**引擎会在使用的时候 自动将其转化为对象**
```js
console.log(str[0]); // I
console.log(str.length); // 13
// 数值字面量和布尔字面量也是这样 -> 转化为Number 才能使用上面的方法
console.log(123.123.toFixed(2)); // 123.12
```
**注意点:**
1. null和undefined没有构造函数形式 只有字面量形式
2. date只有构造函数形式 没有字面量形式
3. Object、Array、RegExp、Function无论用文字还是构造函数的形式都是对象,使用字面量形式更加方便一些,使用构造函数的形式可以提供额外的选项(特殊需要的时候再使用)
### 3.3 内容
**1.对象的内容存储在哪里?存储在容器本身?**
- 存储在容器本身的是属性的名字
- 属性名是指针 -> 指向内容空间,值存储在空间里面
- 为什么要这么设计呢?性能的问题,对象的数据量大,不确定的,放在堆里面是更好的。
```js
{
result: [x,x,x,x,x,x,,x]
}
```
**2.对象的访问的方式有哪些?**
属性访问和键访问。
```js
obj = {
a: 123
}
```
比如`obj.a是属性访问和obj['a']是键访问`。访问的是同一个位置,返回的是同一个值
**3.两种访问方式有什么区别?**
obj.name必须满足`标识符的命名规范`。(必须是变量)
obj["name"],原话是name必须符合`符合UTF-8/Unicode”字符串`,(跟的是字符串或者**变量**)
```js
let myObject = {
a: 2
}
let idx
if (wantA) {
idx = 2
}
console.log(myObject[idx])
```
**4.键值对访问的注意点**
```js
let myObject = {
}
myObject[true] = 'foo'
myObject[3] = 'bar'
myObject[myObject] = 'baz'
myObject['true'] // 'foo'
myObject['3'] // bar
myObject[myObject] // 其实被转化为 -> myObject["[object object]"] baz
```
**说明**:对象的键值访问时,如果是布尔、数字、对象,最终都被转化为字符串。**除了字符串以外的数据类型**,都会被这么转化。
#### 3.3.1 可计算属性名
```js
let prefix = 'foo'
var myObject = {
[prefix + 'bar']: 'hello bar',
[prefix + 'baz']: 'hello baz'
}
console.log(myObject['foobar'])
console.log(myObject['foobaz'])
```
说明:对象的属性名复杂,涉及运算,“字符串拼接”
**还可以和Symbol的结合**
```js
let symbol = Symbol(10)
console.log(symbol.description);
let obj = {
[symbol.description]: 123
}
console.log(obj['10']); // 123
```
#### 3.3.2 属性和方法
```js
let obj = {
foo: function () {
console.log(10)
}
}
```
**function() {console.log(10)}这个函数是否属于obj这个对象?**
1. 某个函数,属于对象,就被称为方法。那么访问obj.foo就会被称为方法访问
2. 但是从`技术角度`来说,函数永远不会属于一个对象 => 把对象内部引用的函数称为方法,有点不妥 => 因为没有函数属于某个对象,函数和对象的关系是间接的关系
```js
function foo () {
console.log('foo')
}
let someFoo = foo // 让一个变量引用这个函数
let obj = {
myObject: foo // 对象里面的一个属性引用的了这个函数
}
console.log(foo)
console.log(someFoo)
console.log(obj.myObject)
```
3. someFoo和obj.myObject是不同的方式引用了foo函数,不能说foo属于obj对象
4. 唯一区别就是,函数打印this。那么someFoo和obj.myObject会有很大的区别
5. 保险的说法:JS中的“函数”和“方法”是可以互换的
>我认为没有必要去纠结这个
#### 3.3.3 数组
1.可以访问数组的下标和length
2.也可以直接给数组添加属性,不推荐这样做
3.给数组添加内容,采用 myArr['3'] 会被自动转化为数组
```js
// 1.可以访问数组的下标和length
var myArr = ['foo', 42, 'bar']
console.log(myArr[0])
console.log(myArr.length)
console.log(myArr[2])
// 2.也可以直接给数组添加属性,不推荐这样做
myArr.baz = 'baz'
console.log(myArr.baz) // baz
console.log(myArr.length) // 3
// 3.
myArr['3'] = 'three'
console.log(myArr[3]) // three
```
#### 3.3.4 复制对象
**案例1**
```js
function anotherFunction() {
/* */
}
let anotherObject = {
c: true
}
let anotherArray = []
let myObject = {
a: 2,
b: anotherObject,
c: anotherArray,
d: anotherFunction
}
anotherArray.push(anotherObject, myObject)
console.log(anotherArray);
```
1. 如果是**浅拷贝**呢,就没有问题,赋值a,就是a的值;赋值bcd,就是复制指针,内容是一样的。
2. 但是如果是**深拷贝**呢?复制的就是值本身了,就会造成循环引用。let obj2 = JSON.parse(JSON.stringify(myObject)) 就不适合转化 循环引用的对象
**json安全的意思**:也就是说,可以被序列化为一个JSON字符串并且根据这个字符串解析出一个结构和值完全一样的对象。才适合`JSON.parse(JSON.stringify(myObject))`
- 该API的缺陷,无法解决循环引用的问题
- 无法序列化函数
- 无法拷贝 set map regExp
**案例2** **Object.assign**
```js
let obj = {
name: '123',
age: '456',
info: {
height: 190
}
}
let obj2 = Object.assign({}, obj)
console.log(obj);
console.log(obj2);
obj.name = '8888888'
obj.info.height = '99999999'
console.log(obj); // name是8888888 info两个对象都会发生变化
console.log(obj2); // name还是123
```
**书上的案例**
Object.assign是浅拷贝,引用数据类型,拷贝的是指针,
```js
let newObj = Object.assign({}, myObject)
console.log(newObj.a); // 2
console.log(newObj.b === anotherObject); // true
console.log(newObj.c === anotherArray); // true
console.log(newObj.d === anotherFunction); // true
```
**for in 也是 浅拷贝**
```js
let obj3 = {}
for (let key in obj) {
obj3[key] = obj[key]
}
console.log(obj3);
obj3.name = '888'
obj3.info.height = 200
console.log(obj3); // name是 '888'
console.log(obj); // name是 '123'
```
#### 3.3.5 属性描述符
**1.每个属性身上都有属性描述符**
```js
var myObject = {
a: 2
}
console.log(Object.getOwnPropertyDescriptor(myObject, 'a'));
// 打印出来是 value writable enumerable configurable
```
**2.修改值 value**
```diff
// writable 能否修改
let obj = {
name: 'hangge',
age: 20
}
obj.name = 'xiaobai'
console.log(obj); // xiaobai
Object.defineProperty(obj, 'name', {
writable: false
})
+obj.name = 'hangge'
+console.log(obj); // xiaobai 修改失败
```
**3.是否可以再次配置属性描述符 configurable**
```js
// configurable
Object.defineProperty(obj, 'name', {
configurable: false
})
// TypeError
Object.defineProperty(obj, 'name', {
configurable: true
})
// 这个是能够修改成功的 但是writable 由 false -> true是不行的
Object.defineProperty(obj, 'name', {
writable: false
})
console.log(Object.getOwnPropertyDescriptor(obj, 'name'));
```
**不能够删除这个属性**
```js
console.log(Reflect.deleteProperty(obj, 'name')); // false
console.log(obj);
```
**4.enumerable 能够遍历枚举** **可枚举性**
```js
Object.defineProperty(obj, 'age', {
enumerable: false
})
for (let key in obj) {
console.log(key, obj[key]); // 只有name xiaobai 因为age已经是不可以枚举的了
}
```
#### 3.3.6 不可变性
目标:希望对象不可变,或者是对象的属性不可变
##### **对象常量**
1. 对象常量,通过Object.defineProperty()API的writable和configurable两个属性,
- 不可以修改已有属性值
- 不可以重新定义
- 不可以删除属性
- 针对的是一个属性。可以添加新的属性
```js
var myObject = {}
Object.defineProperty(myObject, 'FAVOURITE_NUMBER', {
value: 42,
// 不可以修改
writable: false,
// 不可以删除
configurable: false
})
console.log(myObject);
// 修改
myObject.FAVOURITE_NUMBER = 43
console.log(myObject);
// 删除
Reflect.deleteProperty(myObject, 'FAVOURITE_NUMBER')
console.log(myObject);
// 可以增加新的属性
myObject.a = 3
console.log(myObject);
// 重新定义
// Object.defineProperty(myObject, 'FAVOURITE_NUMBER', {
// value: 43,
// // 不可以修改
// writable: false,
// // 不可以删除
// configurable: false
// })
```
##### **禁止扩展**
2. 禁止扩展,通过Object.preventExtensions()的API
- 可以修改已有属性的值
- 可以重新定义已有的属性
- 可以删除已有的属性
- 不能添加新的属性
```js
let myObject2 = {
a: 2
}
myObject2.a = 3
console.log(myObject2); // 修改
Reflect.deleteProperty(myObject2, 'a')
console.log(myObject2); // 可以删除
Object.preventExtensions(myObject2)
myObject2.b = 3
console.log(myObject2); // {a: 2} 创建b属性静默失败了
```
##### **密封 Object.seal()**
3. 密封对象
- 可以修改属性的值
- 删除新属性失败
- 添加新属性失败
```js
let myObject3 = {
a: 2
}
Object.seal(myObject3);
// 可以修改属性的值
myObject3.a = 3
console.log(myObject3.a); // 3
// 添加新属性失败
myObject3.b = 4
console.log(myObject3); // 没有b
// 删除现有属性失败
Reflect.deleteProperty(myObject3, 'a')
```
##### **冻结:Object.freeze(myObject4)**
4. 冻结对象
修改、添加、删除新的属性都失败
```js
let myObject4 = {
a: 2
}
Object.freeze(myObject4)
// 修改属性的值失败
myObject4.a = 3
console.log(myObject4.a); // 2
// 添加新属性失败
myObject4.b = 4
console.log(myObject4); // 没有b
// 删除现有属性失败
Reflect.deleteProperty(myObject4, 'a')
console.log(myObject4); // 没有b
```
##### 深度冻结所有的对象
- 先freeze 整个对象
- 遍历对象的所有属性 单独调用Object.freeze()
```js
let obj6 = {
age: 20
}
let obj5 = {
name: '123',
foo: obj6
}
Object.freeze(obj5)
console.log(obj5);
obj5.foo.age = 30
console.log(obj5); // age是能被修改
Object.keys(obj5).forEach(key => {
Object.freeze(obj5[key])
})
obj5.foo.age = 40
```
#### 3.3.7 [[Get]]
> 访问一个对象的属性时,有一个细节。
```js
var myObject = {
a: 2
}
console.log(myObject.a);
```
1. 在对象中查找是否有名称相同的属性,找打就会返回这个值
2. [[Get]]算法会执行另外一种非常重要的行为。遍历可能存在的原型链。[[Prototype]]
3. 如果无论如何都没有找到名称相同的属性,[[Get]]操作会返回值undefined
>区分,对象的属性值是undefined | 对象压根没有这个属性
```js
// 访问时,[[Get]]是怎么操作的?
var myObject = {
a: 2
}
console.log(myObject.a);
console.log('-----------------------------------------');
let myObject2 = {
a: undefined
}
console.log(myObject2.a); // undefined
console.log(myObject2.b); // undefined
```
1. 底层[[Get]]对myObject2.b的返回值的操作更加的复杂
2. 通过返回值 无法区分这两种情况,稍后会介绍
#### 3.3.8 [[Put]]
>给对象的某个属性赋值是,触发Put,到底是什么个机制呢?
如果已经存在这个属性,[[Put]]算法会做下面的检查
1. 属性是否是**访问描述符**[Getter和Setter],如果是并且存在就调用setter
2. 属性的**数据描述符**[value/writable/enumerable/configurable]种的writable是否是false呢?如果是的话,修改就会失败。严格模式,TypeError
3. 如果都不是,就直接将该值设置为属性的值。
4. 如果对象中,**没有这个属性**,[[Put]]操作会更加的复杂,我们后续会继续讨论。
```js
let obj = {
name: 123
}
Object.defineProperty(obj, 'name', {
writable: false
})
obj.name = '455'
console.log(obj); // 失败
```
```js
"use strict";
let obj = {
name: 123
}
Object.defineProperty(obj, 'name', {
writable: false
})
obj.name = '455'
console.log(obj); // type error
```
#### 3.3.9 Getter和Setter
1. getter和setter的操作 都只能应用到单个属性上面,无法应用到整个对象上。
2. getter和setter都是隐藏的函数
当给一个属性定义getter/setter/或者两者都有时,JavaScript会忽略他们的value和writable属性,取而代之的是getter和setter特性 + configurable + enumerable
**给a属性定义值为2,给b属性定义的值是 a * 2**
```js
let myObject = {
get a() {
return 2
}
}
Object.defineProperty(myObject, 'b', {
// 描述符
get: function () {
// 使得b属性的值是4
return this.a * 2
},
// b属性能够被访问的到
enumerable: true
})
console.log(myObject.a); // 2
console.log(myObject.b); // 4
```
**定义了getter但是没有setter会怎么样**
```js
let myObject2 = {
get a() {
return 2
}
}
console.log(myObject2.a); // 2
myObject2.a = 3
console.log(myObject2.a); // 2 上面的赋值是不管用
```
**但是为了操作合理,我们还是希望能够 getter和setter一起去设置**
```js
let myObject3 = {
get a() {
return this._a_
},
set a(val) {
this._a_ = val * 2
}
}
myObject3.a = 3
console.log(myObject3.a); // 6
```
小结:
1. getter和setter的出现,会覆盖数据描述符value和writable
2. 定义了getter但是咩有定义setter,就会出现一个问题,赋值失效
3. 如果同时有getter和setter,是极好的
#### 3.3.10 存在性
**in判断的是属性是否在这个对象+它的原型链上**
```js
let obj = {
a: 2
}
// obj.__proto__.b = '3'
console.log('a' in obj); // true
console.log('b' in obj); // false
// hasOwnProperty('')判断是仅仅是对象里面有无这个属性
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // false
```
**in能够判断原型 但是hasOwnProperty不能**
```js
let obj = {
a: 2
}
obj.__proto__.b = '3'
console.log('a' in obj); // true
console.log('b' in obj); // true
// hasOwnProperty('')判断是仅仅是对象里面有无这个属性
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // false
```
如果是**Object.create(null)**创建的对象 那么 hasOwnProperty()就会失效呢 **而且报错**
- 对象没有连接到Object.prototype上,这个时候就会不具备hasOwnProperty方法
```js
let obj2 = Object.create(null)
obj2.c = '123'
// console.log(obj2.hasOwnProperty('c')); // obj2.hasOwnProperty is not a function
// 更加强硬的方法
```
此时使用 调用**强行绑定**
```js
// 更加强硬的方法
console.log(Object.prototype.hasOwnProperty.call(obj2, 'c')); // true
```
**数组也可以使用 in**
数组的 2 in [2, 4, 6] 为什么是错的呢?
因为 [2,4,6]包含的属性名是0 1 2 没有4
**enumerable 可枚举性 就是 可以出现在对象属性的遍历中**
```js
var myObject = {}
Object.defineProperty(myObject, 'a', {
enumerable: true,
value: 2
})
Object.defineProperty(myObject, 'b', {
enumerable: false,
value: '3'
})
console.log(myObject.b); // 3
console.log(myObject.hasOwnProperty('b')); // true
for (let k in myObject) {
console.log(k, myObject[k]); // a 2 没有b
}
```
数组也是可以枚举的,但是最好不要用 for in

**另一种方式 来判断对象是否是可以枚举的**
1. Object.keys返回数组,只有可以枚举的
2. propertyIsEnumerable,返回一个数组,无论是否可以枚举
3. in和hasOwnProperty区别在于是否会去查找原型链
4. Object.keys和propertyIsEnumerable都只会查找自身,不会去查找原型链
5. propertyIsEnumerable 检查给定的元素是否在对象中,并且满足enumerable: true
```js
var myObject3 = {}
Object.defineProperty(myObject3, 'a', {
enumerable: true,
value: 2
})
Object.defineProperty(myObject3, 'b', {
enumerable: false,
value: '3'
})
console.log(myObject3.propertyIsEnumerable('a')); // true
console.log(myObject3.propertyIsEnumerable('b')); // false
console.log(Object.keys(myObject3)); // 'a'
console.log(Object.getOwnPropertyNames(myObject3))['a', 'b']
```