一、原型

原型是顺应人类自然思维的产物。中文中有个成语叫做“照猫画虎”,这里的猫看起来就是虎的原型,所以,由此我们可以看出,用原型来描述对象的方法可以说是古已有之。 我们在上一节讲解面向对象的时候提到了:在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象。 还有一种就是基于原型的编程语言,它们利用原型来描述对象。我们的 JavaScript 就是其中代表。

image.png

  1. Function.prototype.__proto__ === Object.prototype //true
  2. __proto__所有对象都有,prototype只有函数才有。
  3. 应用层面:
    每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。
    每个对象都有 proto 属性,指向了创建该对象的构造函数的原型。
  4. 原型链和原型对象一一对应,原型链一定指向原型对象。

    原型和原型链

  • 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出 undefined;
  • 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。

    1. //js引擎部分
    2. <script>
    3. const l = console.log
    4. // 一、 2个顶级原型对象
    5. l(Object.prototype) // ->{constructor:f} 普通对象
    6. l(Function.prototype) // -> ƒ () { [native code] } 函数对象
    7. // 二、他们的构造函数
    8. l(Object.prototype.constructor) // ->ƒ Object() { [native code] }
    9. l(Function.prototype.constructor === Function) // -> ƒ Function() { [native code] }
    10. // 三、他们的原型链关系
    11. l(Object.prototype.__proto__) // ->null
    12. l(Function.prototype.__proto__) // ->{constructor:f} Object.prototype
    13. // 四、利用Function.prototype,内置了很多高阶函数对象(一百多种)
    14. l(Array.__proto__ === Function.prototype) // -> true
    15. l(Boolean.__proto__ === Function.prototype) // -> true
    16. l(Date.__proto__ === Function.prototype) // -> true
    17. l(eval.__proto__ === Function.prototype) // -> true
    18. l(Function.__proto__ === Function.prototype) // -> true 悖论:自己产生了自己?为了逻辑的完整性
    19. </script>
    20. // 以上全是浏览器集成的js引擎提供的机制
    1. //应用层
    2. <script>
    3. // 五、应用层之函数:所有函数都是 new Function生成的。
    4. const date2 = new Date()
    5. l(date2)
    6. /* Date.prototype.eat = function () {
    7. l('吃了苹果')
    8. }
    9. const date1 = new Date()
    10. date1.eat()
    11. l(date1)
    12. l(date1.getDate())
    13. l(date1.getFullYear())
    14. l(date1.getMonth()) */
    15. Date.prototype.eat = new Function()
    16. const date1 = new Date()
    17. date1.eat()
    18. // 六、应用层之对象:所有对象都是 new Ojbect生成的。
    19. let obj1 = {} // let obj1 = new Object()
    20. l(obj1.__proto__ === Object.prototype) //true
    21. </script>

    [[prototype]] 和 proto

    旧版浏览器有proto,新版浏览器没有proto
    es6为了保证新旧版浏览器的兼容性,无论是否支持proto属性,都纳入了规范。但是不推荐使用。
    mdn 资料

    二、数据类型

    不同语言类型对比

    JavaScript基础知识 - 图2
    image.png

参考:
image.png
image.png
image.pngqqqq
image.pngimage.png‘’‘’
image.png
image.png000
image.png
image.png000
image.png

内置类型

JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。

基本类型

六种:null,undefined,Boolean,Number,String,Symbol

内置类型 定义 备注
Null 定义了还是空的 只有一个值
null === null // true
值 null 特指对象的值未设置

1. 原型链的终点
Undefined 未定义的属性或变量 只有一个值,原始值undefined
undefined === undefined // true

1. 设计失误:undefined不是关键字,用void 0来代替。
1. 它是一个JavaScript的 原始数据类型
Boolean 表示逻辑意义上的真和假 有两个值:
关键字true和false
Number 表示通常意义上的数字。
有n个值
1. 符合 IEEE 754-2008 规定的双精度浮点数规则
1. 浮点数的精度问题::有些小数以二进制表示位数是无穷的,
String 表示文本数据 有n个值
1. JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。
1. JavaScript 这个设计继承自 Java,最新标准中是这样解释的,这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。
symbol 唯一标识 有n个唯一的值
1. 它是一切非字符串的对象 key 的集合,在 ES6 规范中,整个对象系统被用 Symbol 重塑。

undefined

概念区分:

  • js中什么是未定义?两种情况
    1. 没有声明变量,直接使用该变量,报错该变量未定义。
    2. 声明了变量,没有赋值。
  • “变量声明了没有赋值” —-> 变量定义了,浏览器默认赋值为undefined。

    object

    | object |
    - 广义上:表示对象的意思,是一切有形和无形物体的总称。
    - 在 js 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是 key-value 结构,key 可以是字符串或者 Symbol 类型。
    | 有普通对象、函数对象、原型对象几种 |
    1. 关于类和类型:
    其他语言(java)每个类都是一个类型,二者几乎等同。可以自定义类型。
    javascript只有七种类型,不可以自定义类型;类只是运行时对象的一个私有属性。
    | | —- | —- | —- | —- |
  1. 基本类型在对象类型中的亲戚:Number;String;Boolean;Symbol。
  2. Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象;symbol不能与new使用
  3. Number、String 和 Boolean直接调用时,它们表示强制类型转换;Symbol直接调用则产生一个symbol对象。
  4. js模糊了对象与基本类型的关系:基本类型可以直接使用对应的对象方法。

    1. console.log("abc".charAt(0)); //a

    甚至我们在原型上添加方法,都可以应用于基本类型。

    1. Symbol.prototype.hello = () => console.log("hello");
    2. var a = Symbol("a");
    3. console.log(typeof a); //symbol,a并非对象
    4. a.hello(); //hello,有效

    关于symbol 的背景

  5. es6之前key的类型只能为字符串。

    1. 所有键名都是字符串,加不加引号都行。
    2. 键名是数字时,自动转为字符串。
    3. 键名不符合规范,语法报错。
  6. symbol 是 ES6 中引入的新类型,它是一切非字符串的对象 key 的集合,在 ES6 规范中,整个对象系统被用 Symbol 重塑。
  7. 为什么引入?
    字符串key容易造成属性名的冲突,引入了symbol作为key。
  8. 为什么不new生成?
    这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

    类型判断

    1、typeof

    存在哪些问题?

  9. 对于基本类型,除了null会错误显示为”object”(最初使用低位存储变量的类型信息造成的),其他正确显示;

  10. 对于对象除了函数显示为”function”,数组和对象都显示为’object’,不能区分array和object;

为什么有这个问题?

  1. let bool = true;
  2. let num = 1;
  3. let str = 'abc';
  4. let und= undefined;
  5. let nul = null;
  6. let arr = [1,2,3,4];
  7. let obj = {name:'xiaoming',age:22};
  8. let fun = function(){console.log('hello')};
  9. let s1 = Symbol();
  10. console.log(typeof bool); //boolean
  11. console.log(typeof num);//number
  12. console.log(typeof str);//string
  13. console.log(typeof und);//undefined
  14. console.log(typeof nul);//object
  15. console.log(typeof arr);//object
  16. console.log(typeof obj);//object
  17. console.log(typeof fun);//function
  18. console.log(typeof s1); //symbol

2、instanceof

  • 优缺点:
    • 不能识别出基本的数据类型
    • 可以检测出引用类型,如array、object、function
    • 同时对于是使用new声明的类型,它还可以检测出多层继承关系。
  • 类型判断原理:
    js的继承都是采用原型链来继承的。比如objA instanceof A ,其实就是看objA的原型链上是否有A的原型,而A的原型上保留A的constructor属性。所以instanceof一般用来检测对象类型,以及继承关系。 ```javascript let bool = true; let num = 1; let str = ‘abc’; let und= undefined; let nul = null; let arr = [1,2,3,4]; let obj = {name:’xiaoming’,age:22}; let fun = function(){console.log(‘hello’)}; let s1 = Symbol();

console.log(bool instanceof Boolean);// false console.log(num instanceof Number);// false console.log(str instanceof String);// false console.log(und instanceof Object);// false console.log(nul instanceof Object);// false console.log(arr instanceof Array);// true console.log(obj instanceof Object);// true console.log(fun instanceof Function);// true console.log(s1 instanceof Symbol);// false

  1. 手写代码实现instanceof
  2. ```javascript
  3. // instanceOf([],Array)
  4. function instanceOf(left, right) {
  5. let leftValue = left.__proto__;
  6. let rightValue = right.prototype;
  7. while (true) {
  8. //Object.__proto --> null
  9. if (leftValue === null) {
  10. return false;
  11. }
  12. // 通过原型链找到了原型对象
  13. if (leftValue === rightValue) {
  14. console.log('left2', leftValue)
  15. console.log('right2', rightValue)
  16. return true;
  17. }
  18. // 沿着原型链查找
  19. console.log('left1', leftValue)
  20. console.log('right1', rightValue)
  21. leftValue = leftValue.__proto__;
  22. }
  23. }
  24. // test
  25. function People() { }
  26. var student = new People()
  27. instanceOf(student, Object)
  28. // student.__proto__ -->People.prototype
  29. // student.__proto__.__proto__ --> Object.Prototype

3、constructor

null、undefined没有construstor方法,因此constructor不能判断undefined和null。
但是它是不安全的,因为contructor的指向是可以被改变。例如:Boolean.prototype.constructor = 'aa'
注意:bool.constructor可以直接.constructor是因为继承了原型对象—Object.prototype的construct属性

  1. // obj.__proto__.constructor === obj.constructor //true
  2. let bool = true;
  3. let num = 1;
  4. let str = 'abc';
  5. let und= undefined;
  6. let nul = null;
  7. let arr = [1,2,3,4];
  8. let obj = {name:'xiaoming',age:22};
  9. let fun = function(){console.log('hello')};
  10. let s1 = Symbol();
  11. console.log(bool.constructor === Boolean);// true
  12. console.log(num.constructor === Number);// true
  13. console.log(str.constructor === String);// true
  14. console.log(arr.constructor === Array);// true
  15. console.log(obj.constructor === Object);// true
  16. console.log(fun.constructor === Function);// true
  17. console.log(s1.constructor === Symbol);//true

4、Object.prototype.toString.call

此方法可以相对较全的判断js的数据类型。

  1. let bool = true;
  2. let num = 1;
  3. let str = 'abc';
  4. let und= undefined;
  5. let nul = null;
  6. let arr = [1,2,3,4];
  7. let obj = {name:'xiaoming',age:22};
  8. let fun = function(){console.log('hello')};
  9. let s1 = Symbol();
  10. console.log(Object.prototype.toString.call(bool));//[object Boolean]
  11. console.log(Object.prototype.toString.call(num));//[object Number]
  12. console.log(Object.prototype.toString.call(str));//[object String]
  13. console.log(Object.prototype.toString.call(und));//[object Undefined]
  14. console.log(Object.prototype.toString.call(nul));//[object Null]
  15. console.log(Object.prototype.toString.call(arr));//[object Array]
  16. console.log(Object.prototype.toString.call(obj));//[object Object]
  17. console.log(Object.prototype.toString.call(fun));//[object Function]
  18. console.log(Object.prototype.toString.call(s1)); //[object Symbol]

至于在项目中使用哪个判断,还是要看使用场景,具体的选择,一般基本的类型可以选择typeof,引用类型可以使用instanceof。

  • 基本类型(null): 使用 String(null)
  • 基本类型(string / number / boolean / undefined) + function: 直接使用 typeof即可
  • 其余引用类型(Array / Date / RegExp Error): 调用toString后根据[object XXX]进行判断
    1. function determineType(obj) {
    2. if (obj == null) return String(obj) //'null'
    3. return typeof obj === 'object'
    4. ? Object.prototype.toString.call(obj) //'[object xxx]'
    5. : typeof obj //'function' 或基本类型
    6. }

    类型转换

    转undefined和null

    其他类型无法转换为undefined和null。

    转boolean类型

    | 其他类型转boolean(6种) | 哪些值或对象可以转为false | 哪些值或对象可以转为true | | —- | —- | —- | | undefined | undefined | 无 | | null | null | 无 | | number | NaN ,0 ,-0 | 剩下的都可以 | | string | 空字符串’’ | 剩下的都可以 | | object | 无 | 都可以 | | symbol | 无 | 都可以 |

需要注意的是:

  1. 基本类型 null 和 undefined 都是值类型(有且只有一个值)。
  2. number类型中特殊的值NaN表示非数值类型,或者不可表示的值。NaN === NaN // false
  3. string类型中转换true和false的依据是,字符串长度是否大于0。例如:Boolean('0') //true
  4. object类型中所有对象都为true。例如:Boolean({}) //trueBoolean([]) //true

    转number类型

    | 其他类型转number(6种) | 哪些值或对象可以转为正常数字 | 哪些值或对象可以转为NaN | | —- | —- | —- | | undefined | 无 | Number(undefined)//NaN | | null | Number(null) // 0 | 无 | | boolean | Number(true) // 1
    Number(false) //0 | 无 | | string | 空字符串、所有有且只有有效数字字符的字符串
    例如:Number('9')//9 | 所有非有效数字字符的非空字符串
    例如:Number('9px')//NaN | | object | 无 | 所有对象 | | symbol | 无法将符号值转换为数字 | |

转string类型

其他类型转string(6种) 哪些值或对象可以转为string
undefined 都是所见即所得,转为对应值的字符串形式,例如:
String(undefined) // "undefined"
String(null) // "null"
String(true) // "true"
String(NaN) // "NaN"
String(Symbol()) // "Symbol()"
null
boolean
number
symbol
object object比较复杂,有多种情况:
1. 普通对象会转为”[object Object]”字符串形式
1. 数组类型中,空数组转为空字符串 “”,非空数组转为序列字符串,例如:

String([])//""
String([67,'34','wow','哇哦'])//"67,34,wow,哇哦"
3. 函数对象所见即所得,例如:
String(function(){console.log('98')})
//"function(){console.log('98')}" |

转object类型

undefined、null、boolean、number、string、symbol这六种基本类型转为亲戚对象,在此过程中基本对象类型经历了隐式转换即装箱操作。原理:基本值 —> 亲戚对象 (装箱转换) —> 可以用任意方法

类型方法

对象方法

方法名 语法 返回值 实现什么功能
assign Object.assign(target, …sources)
参数:
target 目标对象
sources 源对象
目标对象 将所有可枚举属性的值从一个或多个源对象分配到目标对象
create
defineProperties
defineProperty
entries
fromEntries

assign方法

  1. // 1.1 key不冲突时,把资源对象的键值对都补充在目标对象里
  2. const target = { a: 1 }
  3. const result = Object.assign(target, { b: 2 }, { c: 3 })
  4. console.log(target === result) //true
  5. console.log(target) //true
  6. // 1.2 key冲突时,资源对象会覆盖掉目标对象中相同key的值
  7. const target = { a: 1 }
  8. Object.assign(target, { a: 2, b: 2 })
  9. console.log(target) // { a: 2, b: 2 }
  10. // 1.3 数组也可使用这个方法
  11. const target = [1, 2, 7]
  12. Object.assign(target, [3, 4], [5, 6]) //{0:1,1:2}
  13. console.log(target) // [5,6,7]
  14. // 1.4 这个方法是浅拷贝
  15. // 浅拷贝只能拷贝值和对象地址(栈内存中的内容),深拷贝除了他们还可以拷贝对象。(栈和堆内存中的内容)

数组方法

序号 遍历方法 方法作用 入参 回调函数参数 是否改变原数组 返回值
1 for…in… 循环遍历 / / / /
2 for loop / / / /
3 forEach() 回调函数(callback) via系列(value,index,array) 数组中每一项是基本类型是不会改变原数组,复杂类型时会改变原数组 undefined
4 map() 返回一个由回调函数的返回值组成的新数组 返回一个新数组(和原数组等长)
5 filter() 返回一个满足回调函数筛选条件的数组(过滤数据) 返回一个新数组(小于等于原数组长度)

forEach()

  1. var arr1 = [2, 3, 1, 4]
  2. var arr2 = [{ a: 1 }, { a: 2 }]
  3. // 用函数作为参数叫做高阶函数
  4. // 有什么好处?相当于把逻辑片段作为参数传进去;代码更加简洁,封装了重复代码;
  5. arr1.forEach((value, index, arr) => {
  6. value = '333'
  7. });
  8. // 实现forEach
  9. Array.prototype._forEach = function (callback) {
  10. for (var key = 0; key < this.length; key++) {
  11. callback(this[key], key, this)
  12. }
  13. }


序号
增删方法 方法作用 入参 是否改变原数组 返回值
1 push() 在数组尾部添加元素 新增的一个或者多个元素 新数组长度
2 pop() 在数组尾部删除元素 被删除的元素
3 shift 在数组头部删除元素 被删除的元素
4 unshift 在数组头部添加元素 新增的一个或多个元素 新数组的长度
序号 查找元素方法 用途 入参 是否改变原数组 返回值
1 indexOf() 从开头查基本类型的数组 要查找的元素和要从哪开始查找的索引 若查到则返回符合条件的第一个元素的索引,否则是 -1
2 lastIndexOf() 从尾部查基本类型的数组 若查到则返回符合条件的最后一个元素的索引,否则是 -1
3 includes() 判断一个数组是否包含一个指定的值 若查到则返回true,否则返回false
4 find() 用来查找引用类型数组中的满足回调函数条件的元素 via系列的回调函数 若找到返回符合条件的元素,否则返回undefined
5 findIndex() 若找到返回符合条件的元素的索引,否则返回 -1

字符串方法

三、面向对象

js相比较其他语言而言:

  • javaScript(直到 ES6)有对象的概念,但是却没有像其他的语言那样,有类的概念;
  • 在 JavaScript 对象里可以自由添加属性,而其他的语言却不能。
  • 利用各种不同的语言特性来抽象描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java 等流行的编程语言。

如何定义面向对象和基于对象?

  • 面向对象强调编程的思想
    • 面向对象编程也被认为是:更接近人类思维模式的一种编程范式
  • 基于对象:语言和宿主的基础设施由对象来提供,并且 JavaScript 程序即是一系列互相通讯的对象集合

    对象的特征

  • 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。

  • 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
  • 对象具有行为:即对象的状态,可能因为它的行为产生变迁。 | 语言 | 唯一标识性 | 状态 | 行为 | | —- | —- | —- | —- | | C++ | 各种语言的对象唯一标识性都是用内存地址来体现的, 对象具有唯一标识的内存地址,所以具有唯一的标识。 | 成员变量 | 成员函数 | | java | | 属性 | 方法 | | js | | 属性
    (JavaScript 中对象独有的特色是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力) | |

表 - 语言中对象特征对比

语言 描述对象方式
C++
java
基于类
js 基于原型

javascript对象的两种属性描述符

实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。

对象是一个属性的索引结构(索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用 key 来查找 value 的字典)。{writable:true,value:1,configurable:true,enumerable:true}是 value。

为什么有人说“JavaScript 不是面向对象”这样的说法。这是由于 JavaScript 的对象设计跟目前主流基于类的面向对象差异非常大。

可事实上,这样的对象系统设计虽然特别,但是 JavaScript 提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式(下一节课我们会给你介绍 JavaScript 中两种面向对象编程的范式:基于类和基于原型),所以它也是正统的面向对象语言。

JavaScript 语言标准也已经明确说明,JavaScript 是一门面向对象的语言,我想标准中能这样说,正是因为 JavaScript 的高度动态性的对象系统。

面向对象真的需要模拟类吗? JavaScript 本身就是面向对象的,它并不需要模拟,只是它实现面向对象的方式和主流的流派不太一样,所以才让很多人产生了误会。

javaScript 创始人 Brendan Eich 在“原型运行时”的基础上引入了 new、this 等语言特性,使之“看起来语法更像 Java”

数据属性描述符

它比较接近于其它语言的属性概念。数据属性具有四个特征。使用getOwnPropertyDescriptor 查看数据属性。

  • value:就是属性的值。
  • writable:决定属性能否被赋值。
  • enumerable:决定 for in 能否枚举该属性。
  • configurable:决定该属性能否被删除或者改变特征值。

    1. // 1.对象字面量默认定义
    2. let obj = {a:'111'} // 默认定义了四个数据描述符:value writable enumerable configurable
    3. // 2.自定义数据描述符
    4. Object.defineProperty(
    5. {},'a', {value:'222',writable:false,enumerable:false,configurable:false})
    6. console.log(obj1.a)//111
    7. obj2.a = '333';
    8. console.log(obj2.a)//222

    访问器属性描述符

    在大多数情况下,我们只关心数据属性的值即可。第二类属性是访问器(getter/setter)属性,它也有四个特征。使用 Object.defineProperty定义访问器属性或者改变属性的特征。

  • getter:函数或 undefined,在取属性值时被调用。

  • setter:函数或 undefined,在设置属性值时被调用。

    1. // 1.在对象中直接定义
    2. let obj3 = {
    3. b: 'bbb', get b() {
    4. console.log('get') //get
    5. return obj2
    6. }, set b(val) {
    7. console.log('set', val) //set,ccc
    8. }
    9. }
    10. console.log(obj3.b) //{a:'222'}
    11. console.log(obj3.b = 'ccc') //ccc
    12. // 2.defineProperty定义
    13. let obj4 = { r: '111' }
    14. Object.defineProperty(obj4, 'r', {
    15. get() {
    16. console.log('get')//get
    17. return 'eee'
    18. },
    19. set (val) {
    20. console.log('set',val); //set vvv
    21. }
    22. })
    23. console.log(obj4.r) //eee
    24. console.log(obj4.r = 'vvv') //vvv

    注意:不能同时指定访问器和值或可写属性。也就是说只有value writable enumerable configurable或者
    getter setter enumerable configurable可放在一起同时使用。

    运行时

    运行时分为两个阶段:编译阶段、执行阶段
    编译阶段:js通过编译生成执行上下文和可执行代码两部分。
    执行阶段:执行可执行代码,输出结果。

  • 执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

  • 可执行代码就是指js引擎可以执行的代码,这些代码可以是【字节码、机器码】。我们写的js代码它看不懂,也不会执行,它要先把js编译成字节码、机器码才行。

运行时理解参考链接

js到底是什么语言?

  • 解释型语言 —- 逐行对代码进行编译、执行操作
  • 动态语言 —- 运行时才能确定变量类型 链接
  • 弱类型语言 —- 链接

    new

    new的实现原理:
  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

手写一个new函数

  1. /*
  2. * @param 构造函数
  3. * @params 实例对象参数
  4. */
  5. function _new() {
  6. // 创建一个空的对象
  7. let obj = new Object()
  8. // 获得构造函数
  9. let Con = [].shift.call(arguments)
  10. // 链接到原型
  11. obj.__proto__ = Con.prototype
  12. // 绑定 this,执行构造函数
  13. let result = Con.apply(obj, arguments)
  14. // 确保 new 出来的是个对象
  15. return typeof result === 'object' ? result : obj
  16. }
  17. // 测试
  18. function People(name) {
  19. this.name = name
  20. }
  21. let student1 = new People('zs')
  22. console.log(student1)//People{name:'zs'}
  23. let student2 = _new(People, 'ls')
  24. console.log(student2)//People{name:'ls'}
  25. /*
  26. 函数内部的机制:
  27. 1. 每个函数都有一个动态参数 this
  28. 2. 函数有实参和形参。形参在声明函数时声明,实参在调用函数时传入。
  29. 3. 函数有函数体
  30. 4. 每个函数都有返回值,没有用retrun关键字指定返回值,默认在函数体的底部返回undefined
  31. */
  32. function fn(//1.动态参数this //2.形参列表arguments
  33. ) {
  34. // 3.函数体
  35. // 4.返回值 默认是 return undefined
  36. }

知识补充:
关于语法糖的好坏:
1. 好处:只管用,不用知道内部原理。对新手特别友好;语法糖会提升编程效率。
2. 坏处:深入内部原理及其困难。有时候会干扰对代码执行的分析;提高了语法学习的门槛,让学习曲线由易到难。
什么是语法糖:
1. 封装复杂的内部逻辑,变为简单易用的代码。
2. 写的爽,用的爽,就像吃了糖一样。
有哪些语法糖:
1. js中语法糖数不胜数。es6疯狂的添加。常见的就是new关键字 class关键字 function关键字 对象字面量{},instanceof。
2. 学习js对新手的要求就是掌握语法糖的熟练应用;对中级工程师要求深入理解常见语法糖的原理。

this

  • 宿主对象(host Objects):由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。
  • 内置对象(Built-in Objects):由 JavaScript 语言提供的对象。
    • 固有对象(Intrinsic Objects ):由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
    • 原生对象(Native Objects):可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
    • 普通对象(Ordinary Objects):由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。

this指向分为三种情况:

情况一:this指向调用者

  1. // 宿主对象 === 全局对象
  2. function foo() { //var foo = fn() {}
  3. console.log(this) //window对象
  4. console.log(this.a) //1
  5. }
  6. var a = 1;
  7. foo();
  8. /*
  9. this第一条规则总结:
  10. 1. 声明的全局变量都是宿主对象(window)的属性;
  11. 2. this指向在函数定义阶段,无法确定;
  12. 3. this指向在执行阶段(运行时)才能确定。因为this相当于一个动态参数。
  13. */
  1. // 宿主对象 === 全局对象
  2. function foo() { //var foo = fn() {}
  3. console.log(this) // obj
  4. console.log(this.a) //2
  5. }
  6. var obj = {
  7. a: 2,
  8. foo: foo // obj.foo !== window.foo
  9. }
  10. obj.foo()
  11. /*
  12. this解惑
  13. 1. this特点:this只存在函数中,作为动态参数
  14. 2. 函数永远作为某个对象的属性存在,this恰恰指的是函数所在的那个对象。
  15. 3. this表达的意思是,我这个函数是哪个对象的行为。
  16. */

情况二:new和call、bind、apply改变this指向

new会将this指向新生成的对象。

方法 剩余入参(第一个参数传入的都是this要指向的对象) 是否函数调用 应用场景
call 参数序列 实现继承
apply 第二个参数是数组 经常跟数组有关系比如借助于数学对象实现数组最大值最小值
bind 参数序列 常用于回调函数中。比如改变定时器内部的this指向或者传递多个不同的this

call实现继承

  1. script>
  2. function Class1(arg1, arg2) {
  3. // new Class2生成新的实例对象的过程中经过call改变this指向,指定到了Class2上
  4. this.name = arg1;
  5. this.pass = arg2;
  6. this.showSub = function () {
  7. return this.name - this.pass; //5 - 6
  8. }
  9. }
  10. function Class2(arg1, arg2, arg3) {
  11. // this -> Class2
  12. Class1.call(this, arg1, arg2);
  13. this.get3Arg = function () {
  14. return arg3;
  15. }
  16. }
  17. // this -> Class1
  18. var class1 = new Class1(5, 6);//new Class1()与new Class1是一样的作用
  19. console.log(class1.showSub()); //5-6=-1
  20. var class2 = new Class2(10, 1, 12);
  21. console.log(class2.showSub()); //10-1=9
  22. console.log(class2.get3Arg());//第三个参数12
  23. </script>

模拟实现call函数

  1. function foo() {
  2. console.log(this)
  3. }
  4. foo()//window
  5. var arr1 = []
  6. foo.call(arr1))//[]
  7. // call原理
  8. Function.prototype.myCall = function (target) { //形参就相当于 var target = undefined
  9. /*
  10. 实现原理:把目标函数foo绑定在目标对象target上(本来函数的this指向window,但是想要让this指向我们指定的对象,所以有下面这些操作)
  11. 1. 获取目标对象
  12. 2. 获取目标函数 foo,并把目标函数foo绑定到目标对象上
  13. 3. 执行目标函数并返回
  14. */
  15. //如果传参则使用Object()包裹一下,有可能不是一个对象,若没有传参默认为window
  16. target = target ? Object(target) : window
  17. //将targetFunction绑定到target上,targetFunction是定义的临时属性
  18. target.targetFunction = this
  19. //获取传入的除了target以外剩余的参数
  20. let args = [...arguments].slice(1)
  21. let result = target.targetFunction(...args)
  22. //删除临时属性
  23. delete target.targetFunction
  24. return result
  25. }
  26. var arr1 = []
  27. foo.myCall(arr1))//arr1 []

注意:target.targetFunction = this这行代码的意思就是把foo.call(arr1) 中的foo函数挂载到arr1上。当执行foo.myCall() 时, myCall里的 this 指向调用者foo,所以这里的this就是foo函数。
apply求最大最小值

  1. <script>
  2. const l = console.log
  3. var arr = [13, 4, 53, 43, 43, 23]
  4. const min = Math.min.apply(null, arr)
  5. const max = Math.max.apply(null, arr)
  6. const max = Math.max.call(null, ...arr)
  7. l(min) //4
  8. l(max) //53
  9. </script>

apply实现原理

  1. <script>
  2. Function.prototype.myApply = function (context) {
  3. var context = context || window
  4. context.fn = this
  5. var result
  6. if (arguments[1]) {
  7. result = context.fn(...arguments[1])
  8. } else {
  9. result = context.fn()
  10. }
  11. delete context.fn
  12. return result
  13. }
  14. let arr = [1, 34, 66, 4, 0, 4];
  15. let min = Math.min.myApply(window, arr)
  16. let max = Math.max.myApply(window, arr)
  17. console.log(min) //0
  18. console.log(max) //66
  19. </script>

bind在回调函数中使用和实现函数科里化
bind使用场景举例:

  1. function fn() {
  2. console.log(this)
  3. }
  4. var arr = [1, 3]
  5. setTimeout(fn, 100)//window
  6. setTimeout(fn.bind(arr), 100)//arr
  7. // 异步调用this都是指向window

bind实现原理

  1. Function.prototype.bind2 = function (context) {
  2. var self = this; // 这句实际上是把调用bind2的函数赋给self,console.log(self)得到的是一个函数
  3. var args = Array.prototype.slice.call(arguments, 1); // 获取传入的参数,将其变为数组存入args中
  4. return function () {
  5. var funArgs = Array.prototype.slice.call(arguments); // 这里的arguments是这个return函数中传入的参数
  6. return self.apply(context, args.concat(funArgs)) // 将this指向context,将self的参数和return的function的参数拼接起来
  7. }
  8. }

new改变this指向

  1. function foo() { //var foo = fn() {}
  2. console.log(this) // c对象
  3. console.log(this.a) //undefined
  4. }
  5. var c = new foo()
  6. c.a = 3
  7. console.log(c.a) //3

情况三:箭头函数与回调函数

  • 回调函数指向window。
  • 箭头函数不改变this指向。 ```javascript function fn1() { // var self = this const fn2 = () => { // console.log(‘this’, this) } fn2() }

fn1()//window setTimeout(fn1, 100)//window setTimeout(fn1.bind([]), 100)//[] fn1.call(Boolean)//Boolean

  1. ```javascript
  2. function fn1() {
  3. //let that = this;//es6之前没有箭头函数,一般用这种方式存this
  4. const fn2 = () => {
  5. //console.log('this', this)
  6. function fn3() {
  7. console.log('this', this)
  8. }
  9. fn3()
  10. }
  11. fn2()
  12. }
  13. fn1()//window
  14. setTimeout(fn1, 100)//window
  15. setTimeout(fn1.bind([]), 100)//window
  16. fn1.call(Boolean)//window
  17. // 1. 变量和函数的作用域在声明阶段就已经确定了。
  18. // 2. this(每个函数都有的属于自己的动态参数)只能在函数调用时确定。

面向对象编程

面向对象有三个特性:封装、继承、多态

默认指的是基于类的面向对象。

多态

多态的实际定义:同一个操作作用于不同的对象上,可以产生不同的解释和不同的执行结果

也就是说,对于JavaScript一切皆对象(null, undefined除外)的语言来说,将相同的行为作用在不同的对象上时会产生不同的行为结果

比如说,同样是叫声这个行为,猫和狗(两个对象)会产生不同的叫声,实际上这符合JavaScript多态的特征

多态背后的思想就是想“做什么”和 “谁去做”分离开来,也就是将“不变的事物”和“可变的事物”分离开来

以上的例子中可以说明,动物都会叫,这是不变的。而不同的动物怎么叫是可变的

把不变的隔离开来,把可变的封装起来,这给予了我们扩展代码的能力,不管有多少只动物,都可以通过增加代码的方式来实现,而无需改变产生行为的代码

实现多态可以通过构造函数的方式
image.png
image.png

封装

1 原始模式
把两个属性封装在一个对象里面就是最简单的封装了,但是这种方式有两个很明显的缺点:

  • 如果多生成几个实例,写起来就非常麻烦(有多少写多少);
  • 实例与原型之间没有任何办法可以看出有什么联系。
    1. var cat1 = {}; // 创建一个空对象
    2. cat1.name = "大毛"; // 按照原型对象的属性赋值
    3. cat1.color = "黄色";
    4. var cat2 = {};
    5. cat2.name = "二毛";
    6. cat2.color = "黑色";
    1.1 原始模式的改进
    写一个函数解决重复代码的问题
    1. function Cat(name,color) {
    2. return {
    3. name:name,
    4. color:color
    5. }
    6. }
    然后生成实例对象,就相当于调用函数
    1. var cat1 = Cat("大毛","黄色");
    2. var cat2 = Cat("二毛","黑色");
    这种方法的问题依然是,cat1和cat2之间没有内在的联系,不能反映出它们是同一个原型对象的实例。
    2 构造函数模式
    为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式
    所谓”构造函数”,其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。
    比如,猫的原型对象现在可以这样写
    1. function Cat(name,color){
    2. this.name=name;
    3. this.color=color;
    4. }
    生成实例对象
    1. var cat1 = new Cat("大毛","黄色");
    2. var cat2 = new Cat("二毛","黑色");
    3. alert(cat1.name); // 大毛
    4. alert(cat1.color); // 黄色
    这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。
    1. alert(cat1.constructor == Cat); //true
    2. alert(cat2.constructor == Cat); //true
    Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。
    1.  alert(cat1 instanceof Cat); //true
    2. alert(cat2 instanceof Cat); //true
    怎么判断实例和实例、实例和构造函数之间的关系?
    1. constructor 明确了实例和构造函数之间的关系。
    2. instanceof运算符 明确了实例和构造函数之间的关系。
    3. 间接判断实例和实例之前的关系。
    2.1 存在的不足
    构造函数方法很好用,但是存在一个浪费内存的问题。
    请看,我们现在为Cat对象添加一个不变的属性type(种类),再添加一个方法eat(吃)。那么,原型对象Cat就变成了下面这样:
    1.   function Cat(name,color){
    2.     this.name = name;
    3.     this.color = color;
    4.     this.type = "猫科动物";
    5.     this.eat = function(){alert("吃老鼠");};
    6.   }
    生成实例
    1. var cat1 = new Cat("大毛","黄色");
    2. var cat2 = new Cat ("二毛","黑色");
    3. alert(cat1.type); // 猫科动物
    4. cat1.eat(); // 吃老鼠
    表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。
    1. alert(cat1.eat == cat2.eat); //false
    能不能让type属性和eat()方法在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。
    3 Prototype模式
    Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象(例如,Cat.prototype指向堆内存存储的Cat的原型对象)。这个对象的所有属性和方法,都会被构造函数的实例继承。
    这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上
    1.   function Cat(name,color){
    2.     this.name = name;
    3.     this.color = color;
    4.   }
    5.   Cat.prototype.type = "猫科动物";
    6.   Cat.prototype.eat = function(){alert("吃老鼠")};
    生成实例
    1. var cat1 = new Cat("大毛","黄色");
    2. var cat2 = new Cat("二毛","黑色");
    3. alert(cat1.type); // 猫科动物
    4. cat1.eat(); // 吃老鼠
    这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
    1.   alert(cat1.eat == cat2.eat); //true
    3.1 验证方法

isPrototypeOf()
这个方法用来判断,某个proptotype对象和某个实例之间的关系。

  1. alert(Cat.prototype.isPrototypeOf(cat1)); //true
  2. alert(Cat.prototype.isPrototypeOf(cat2)); //true

hasOwnProperty()
每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。

  1. alert(cat1.hasOwnProperty("name")); // true
  2. alert(cat1.hasOwnProperty("type")); // false

in运算符
in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。

  1. alert("name" in cat1); // true
  2. alert("type" in cat1); // true

in运算符还可以用来遍历某个对象的所有属性。

  1. for(var prop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }

继承

构造函数的继承

对象之间的”继承”的五种方法
比如,现在有一个”动物”对象的构造函数。

  1. function Animal(){
  2. this.species = "动物";
  3. }

还有一个”猫”对象的构造函数

  1.  function Cat(name,color){
  2. this.name = name;
  3. this.color = color;
  4. }

怎样才能使”猫”继承”动物”呢?
一、 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

  1. function Cat(name,color){
  2. Animal.apply(this, arguments);
  3. this.name = name;
  4. this.color = color;
  5. }
  6. var cat1 = new Cat("大毛","黄色");
  7. alert(cat1.species); // 动物

二、 prototype模式
第二种方法更常见,使用prototype属性。
如果”猫”的prototype对象,指向一个Animal的实例,那么所有”猫”的实例,就能继承Animal了。

  1. Cat.prototype = new Animal();
  2. Cat.prototype.constructor = Cat;
  3. var cat1 = new Cat("大毛","黄色");
  4. alert(cat1.species); // 动物

Cat.prototype = new Animal();将Cat的prototype对象指向一个Animal的实例。它相当于完全删除了prototype 对象原先的值,然后赋予一个新值。

Cat.prototype.constructor = Cat;任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有”Cat.prototype = new Animal();”这一行,Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal。alert(Cat.prototype.constructor == Animal); //true

每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
alert(cat1.constructor == Cat.prototype.constructor); // true

因此,在运行Cat.prototype = new Animal();这一行之后,cat1.constructor也指向Animal。
alert(cat1.constructor == Animal); // true

这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是第二行的意思。
这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,o.prototype = {};那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。
o.prototype.constructor = o;
三、 直接继承prototype
第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。
我们先将Animal对象改写:

  1. function Animal(){ }
  2. Animal.prototype.species = "动物";

然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。

  1. Cat.prototype = Animal.prototype;
  2. Cat.prototype.constructor = Cat;
  3. var cat1 = new Cat("大毛","黄色");
  4. alert(cat1.species); // 动物

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。
Cat.prototype.constructor = Cat;这行代码其实是有问题的,这一句实际上把Animal.prototype对象的constructor属性也改掉了。alert(Animal.prototype.constructor); // Cat
四、 利用空对象作为中介
由于”直接继承prototype”存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。

  1.   var F = function(){};
  2.   F.prototype = Animal.prototype;
  3.   Cat.prototype = new F();
  4.   Cat.prototype.constructor = Cat;

F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。
alert(Animal.prototype.constructor); // Animal
将上面的方法进行封装便于使用

  1. function extend(Child, Parent) {
  2. var F = function(){};
  3. F.prototype = Parent.prototype;
  4. Child.prototype = new F();
  5. Child.prototype.constructor = Child;
  6. Child.uber = Parent.prototype;
  7. }
  8. //使用
  9. extend(Cat,Animal);
  10. var cat1 = new Cat("大毛","黄色");
  11. alert(cat1.species); // 动物

这个extend函数,就是YUI库如何实现继承的方法。
另外,说明一点,函数体最后一行,Child.uber = Parent.prototype;意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是”向上”、”上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
五、 拷贝继承
上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用”拷贝”方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。
首先,还是把Animal的所有不变属性,都放到它的prototype对象上。

  1. function Animal(){}
  2. Animal.prototype.species = "动物";

然后,再写一个函数,实现属性拷贝的目的。

  1. function extend2(Child, Parent) {
  2. var p = Parent.prototype;
  3. var c = Child.prototype;
  4. for (var i in p) {
  5. c[i] = p[i];
  6. }
  7. c.uber = p;
  8. }

这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。
使用的时候,这样写:

  1. extend2(Cat, Animal);
  2. var cat1 = new Cat("大毛","黄色");
  3. alert(cat1.species); // 动物

组合继承 - 较完美继承方式

普通函数的继承(非构造函数继承)

什么是非构造函数继承?
例如:

  1. //有一个对象叫做“中国人”
  2. var Chinese = {
  3. nation: "中国"
  4. };
  5. //还有另一个对象叫做“医生”
  6. var Doctor = {
  7. career: "医生"
  8. };
  9. //请问怎么样才能让“医生”去继承“中国人”,也就是说怎样才能生成一个“中国医生”这个对象?
  10. //请注意,这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现“继承”。

一、object()方法
object()函数可以解决上述问题

  1. function object(o) {
  2. function F() {}
  3. F.prototype = o;
  4. return new F ();
  5. }
  6. //这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。

使用:

  1. //1、先在父对象的基础上,生成子对象
  2. var Doctor = object(Chinese);
  3. //2、再加上子对象本身的属性
  4. Doctor.career = '医生';
  5. //3、这时,子对象已经继承了父对象的属性了。
  6. console.log(Doctor.nation); //中国

二、浅拷贝

  1. //除了使用"prototype链"以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。
  2.  function extendCopy(p) {
  3.     var c = {};
  4.     for (var i in p) {
  5.       c[i] = p[i];
  6.     }
  7.     c.uber = p;
  8.     return c;
  9.   }

使用:

  1. var Doctor = extendCopy(Chinese);
  2. Doctor.career = '医生';
  3. console.log(Doctor.nation); // 中国

这样做存在哪些问题?

  1. //如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。
  2. //现在给Chinese添加一个"出生地"属性,它的值是一个数组。
  3. Chinese.birthPlaces = ['北京','上海','香港'];
  4. 通过extendCopy()函数,Doctor继承了Chinese
  5. var Doctor = extendCopy(Chinese);
  6. //然后,我们为Doctor的"出生地"添加一个城市:
  7. Doctor.birthPlaces.push('厦门');
  8. console.log(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
  9. console.log(Chinese.birthPlaces); //北京, 上海, 香港, 厦门
  10. //所以,extendCopy()只是拷贝基本类型的数据,我们把这种拷贝叫做"浅拷贝"。这是早期jQuery实现继承的方式。

三、深拷贝
所谓”深拷贝”,就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用”浅拷贝”就行了

  1. function deepCopy(p, c) {
  2. var c = c || {};
  3. for (var i in p) {
  4. if (typeof p[i] === 'object') {
  5. c[i] = (p[i].constructor === Array) ? [] : {};
  6. deepCopy(p[i], c[i]);
  7. } else {
  8. c[i] = p[i];
  9. }
  10. }
  11. return c;
  12. }

使用:

  1. var Doctor = deepCopy(Chinese);
  2. //现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性
  3. Chinese.birthPlaces = ['北京','上海','香港'];
  4. Doctor.birthPlaces.push('厦门');
  5. //这时,父对象就不会受到影响了。
  6. console.log(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
  7. console.log(Chinese.birthPlaces); //北京, 上海, 香港
  8. //目前,jQuery库使用的就是这种继承方法。

执行上下文(Execution context 作用域)

function (context) {}

执行上下文三个重要属性:

  • VO(variate object) 只能在全局作用域访问,局部作用域通过AO(active variable object)访问。
  • 作用域链
  • this

js中什么是变量?大概三种。

  • var/const/let
  • function fn () {}
  • function fn (a,b) {} 函数形参

深度理解变量声明;

  • 在生成执行上下文时,会有两个阶段。
  • 第一个阶段是创建的阶段(具体步骤是创建 VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,
  • 所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。

函数优先于变量提升

let会形成临时死区: