JavaScript进阶
javaScript基础总结
数据类型相关知识点
基本(值)类型
- String: 任意字符串
- Number: 任意的数字
- boolean: true/false
- undefined: undefined
- null: null –>使用
typeof
时返回object
- Symbol(ECMAScript2016新增)。 –>Symbol 是基本数据类型的一种,
Symbol
对象是 Symbol原始值的封装 。 - Bigint –>BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数。
加上下方的 [ 对象 ] 类型,目前 javaScript 有八种数据类型
对象(引用)类型
- Object: 任意对象
- Function: 一种特别的
对象
(可以执行) –内部包含可运行的代码 - Array: 一种特别的
对象
(key
为数值下标属性, 内部数据是有序的)
判断方法
typeof
typeof
操作符返回一个字符串
,表示未经计算的操作数的类型。
可以判断: undefined/ 数值 / 字符串 / 布尔值 / function
不能判断: null与object object与array
注意
: 运行console.log(typeof undefined)
时,得到的的也是一个字符串,同时为小写!!
–>'undefined'
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// typeof返回数据类型的字符串表达
var a
//注意:typeof返回的是字符串
console.log(a, typeof a, typeof a==='undefined',a===undefined ) // undefined 'undefined' true true
console.log(undefined === 'undefined') //false
a = 4
console.log(typeof a==='number') //true
a = 'vbnvn'
console.log(typeof a==='string') //true
console.log(typeof a==='String') //false -->注意,返回的类型为小写
a = true
console.log(typeof a==='boolean') //true
a = null
console.log(typeof a, a===null) // 'object' true
let b={}
console.log(typeof b,typeof null) // 'object' 'object' -->所以Typeof不能判断null与object
instanceof
(判断实例方法)
专门判断对象
的具体类型instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20var b1 = {
b2: [1, 'abc', console.log],
//可以简化成 b3:()=>()=> 'zxc' -->高阶函数相关知识
b3: function () {
return () =>{ return 'zxc'}
}
}
/**使用instanceof进行对象判断*/
console.log(b1 instanceof Object, b1 instanceof Array) // true false
console.log(b1.b2 instanceof Array, b1.b2 instanceof Object) // true true
console.log(b1.b3 instanceof Function, b1.b3 instanceof Object) // true true
/**使用typeof进行对象中某属性的判断*/
console.log(typeof b1.b2, typeof null) // 'object' 'object'
console.log(typeof b1.b3==='function') // true
console.log(typeof b1.b2[2]==='function') //true
/**调用对象与数组中某函数示例*/
b1.b2[2]('调用console.log打印zxc') //调用console.log打印zxc
console.log(b1.b3()()) // zxc
===
可以判断: undefined, null
简而言之,在比较两件事情时,双等号将执行类型转换
;三等号将进行相同的比较,而不进行类型转换
(如果类型不同, 只是总会返回 false )
相关问题引出
undefined与null的区别?
undefined代表定义未赋值
nulll定义并赋值了, 只是值为null
代码示例
1
2
3
4var a
console.log(a) // undefined
a = null
console.log(a) // null
什么时候给变量赋值为null呢?
初始赋值, 表明将要赋值为对象,
可以用做约定俗成的占位符
结束前,
让对象成为垃圾对象
(被垃圾回收器回收)代码示例
1
2
3
4
5
6//起始,可以用做约定俗成的占位符
var b = null // 初始赋值为null, 表明将要赋值为对象
//确定对象就赋值
b = ['atguigu', 12]
//最后在不使用的时候,将其引用置空,就可以释放b这个对象占用的内存 ---当没有引用指向它的对象称为垃圾对象
b = null // 让b指向的对象成为垃圾对象(被垃圾回收器回收)
严格区别变量类型与数据类型?
js的变量本身是没有类型的, 变量的类型实际上是变量内存中数据的类型
- 数据的类型
- 基本类型
- 对象类型
- 变量的类型(变量内存值的类型)
- 基本类型: 保存就是
基本类型
的数据 - 引用类型: 保存的是
地址值(对象类型)
- 基本类型: 保存就是
补充知识点:
字符串比较*>
、<
以及charCodeAt()
*方法
- Javascript字符串在进行大于(小于)比较时,会根据第一个不同的字符的ascii值码进行比较,当数字(number)与字符串(string)进行比较大小时,会强制的将数字(number)转换成字符串(string)然后再进行比较
1 | (function(){ |
- 手动转换为ascii后相减,用正负数表示大小
1 | sorter={(a:string,b:string)=> a.charCodeAt()-b.charCodeAt()} |
数据,变量, 内存的理解
什么是数据?
- 存储在内存中代表特定信息的’东西’, 本质上是0101…
- 数据的特点:
可传递
,可运算
–>let a=0;b=a 🔜体现可传递 - 一切皆数据
- 内存中所有操作的目标: 数据
- 算术运算
- 逻辑运算
- 赋值
- 运行函数
什么是内存?
内存条通电后产生的可储存数据的空间(临时的)
内存产生和死亡: 内存条(电路版)==>通电==>产生内存空间==>存储数据==>处理数据==>断电==>内存空间和数据都消失
一块小内存的2个数据
- 内部存储的数据
- 地址值
- 内存分类
- 栈: 全局变量/局部变量
- 堆: 对象
什么是变量?
- 可变化的量, 由变量名和变量值组成
- 每个变量都对应的一块小内存, 变量名用来查找对应的内存, 变量值就是内存中保存的数据
ps:变量obj.xx
–>.
相当于拿着地址找到后面对应的内存,所以只有当我变量中存的是地址,才可以用.
内存,数据,变量三者之间的关系
- 内存用来存储数据的空间
- 变量是内存的标识
相关问题引出
关于赋值和内存的问题
let a = xxx, a内存中到底保存的是什么?
- xxx是基本数据, 保存的就是这个数据
- xxx是对象, 保存的是对象的地址值
- xxx是一个变量, 保存的xxx的内存内容(可能是基本数据, 也可能是地址值)
关于引用变量赋值问题
2个引用变量指向同一个对象, 通过一个变量修改对象内部数据, 另一个变量看到的是修改之后的数据
2个引用变量指向同一个对象, 让其中一个引用变量指向另一个对象, 另一引用变量依然指向前一个对象
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13let a = {age: 12}
//此时是将a指向的地址值赋值给B,所以B此时也指向{age:12}这个内存
let b = a
//此时重新创建了一个内存并让a指向它,所以此处a指向的是{name:'hong'},而b指向仍是刚开始的指向{age:12}
a = {name: 'hong'}
//此时a与b指向的内存已经不一样了,所以修改互不影响
b.age = 14
console.log(b.age, a.name, a.age) // 14 hong undefined
//此时其实已经重新创建了一个内存{age:15},并且将其地址赋值覆盖给a
//实际上传进来的obj也是拿着其key对应的地址值找内存,此时
const fn2=(obj) => obj = {age: 15}
fn2(a)
console.log(a.age) //15
在js调用函数时传递变量参数时, 是值传递还是引用传递
理解1: 都是值(基本/地址值)传递
所以实际上传进function中的参数也是拿着其存着的地址值找内存
1
2
3
4
5//传进来的obj存储的是a中存的地址值,所以obj==a(因为他们地址值一致,指向一致)
let a = {name: 'wong'}
const fn2=(obj) => obj = {age: 15}
fn2(a)
console.log(a.age) //15理解2: 可能是值传递, 也可能是引用传递(地址值)
JS引擎如何管理内存?
- 内存生命周期
- 分配小内存空间, 得到它的使用权
- 存储数据, 可以反复进行操作
- 释放小内存空间
- 释放内存
- 局部变量: 函数执行完自动释放
- 对象: 成为垃圾对象==>垃圾回收器回收
1 | var a = 3 |
对象
对象的概念
什么是对象?
- 多个数据的封装体
- 用来保存多个数据的容器
- 一个对象代表现实中的一个事物
为什么要用对象?
统一管理多个数据
对象的组成
- 属性: 属性名(字符串)和属性值(任意)组成
- 方法: 一种特别的属性(属性值是函数)
如何访问对象内部数据?
.属性名
: 编码简单, 有时不能用['属性名']
: 编码麻烦, 能通用
什么时候必须使用['属性名']
的方式?
- 属性名包含特殊字符:
-
空格
- 属性名不确定
1 | var p = {} |
函数
函数的概念
什么是函数
- 实现特定功能的n条语句的封装体
- 只有函数是可以执行的, 其它类型的数据不能执行
为什么要用函数?
- 提高代码复用
- 便于阅读交流
如何定义函数?
- 函数声明
- 表达式
1 | function fn1 () { console.log('fn1()' )//函数声明 |
如何调用(执行)函数
- test(): 直接调用
- obj.test(): 通过对象调用
- new test(): new调用
test.call/apply(obj)
: 临时让test成为obj的方法进行调用
- 代码示例
1 | var obj = {} |
回调函数
什么函数才是回调函数?
- 你定义的
- 你没有调
- 但最终它执行了(在某个时刻或某个条件下)
常见的回调函数?
- dom事件回调函数 ==>发生事件的dom元素
- 定时器回调函数 ===>window
- ajax请求回调函数
- 生命周期回调函数
1 | // dom事件回调函数 |
IIFE (自调用函数)
全称:
Immediately-Invoked Function Expression
自调用函数作用:
隐藏实现
不会污染外部(一般指全局)命名空间
用它来编码js模块
代码示例
1 | (function () { //匿名函数自调用 |
函数中的this
this是什么?
- 任何函数本质上都是通过某个对象来调用的,如果没有直接指定就是
window
- 所有函数内部都有一个变量
this
- 它的值是
调用函数的当前对象
如何确定this的值?
- test(): window
- p.test(): p
- new test(): 新创建的对象
- p.call(obj): obj
代码举例详解
1 | function Person(color) { |
关于语句分号
- js一条语句的后面可以不加分号
- 是否加分号是编码风格问题, 没有应该不应该,只有你自己喜欢不喜欢
- 在下面2种情况下不加分号会有问题
小括号开头的前一条语句
中方括号开头的前一条语句
- 解决办法:
在行首加分号
- 强有力的例子: vue.js库
函数高级
原型与原型链
原型 [prototype]
- 函数的
prototype
属性
- 给原型对象添加属性(
一般都是方法
)
- 作用: 函数的所有实例对象自动拥有原型中的属性(方法)
- 代码示例
1 |
|
显式原型与隐式原型
每个函数function都有一个
prototype
,即显式
原型(属性)每个实例对象都有一个[
__ proto __
],可称为隐式
原型(属性)==对象的隐式原型的值为其对应构造函数的显式原型的值==
内存结构
总结:
- 函数的[
prototype
]属性: 在定义函数时自动添加的, 默认值是一个空Object对象 - 对象的[
__ proto __
]属性: 创建对象时自动添加的,默认值为构造函数的prototype属性值
- 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
- 代码示例:
1 | //定义构造函数 |
补充
[[prototype]]
和__proto__
两者表示的意义是一样的,都是表示对象的内部属性,指向该对象的原型。前者是在一些有关资料书籍使用[[prototype]]
表示一个对象的原型属性。而__proto__
主要在浏览器实现中,使用该标志标识其内部属性。
但目前浏览器中两者都存在,不知道有什么区别。
原型链
原型链
- 访问一个对象的属性时,
- 别名: 隐式原型链
- 作用: 查找对象的属性(方法)
构造函数/原型/实例对象的关系(图解)
1 | var o1 = new Object(); |
1 | function Foo(){ } |
ps:所有函数的[__ proto __
]都是一样的
完整图解
补充
函数的显示原型指向的对象默认是空Object实例对象(但Object不满足)
1 | console.log(Fn.prototype instanceof Object) // true |
所有函数都是Function的实例(包含Function)
1 | console.log(Function.__proto__===Function.prototype) |
Object的原型对象是原型链尽头
1 | console.log(Object.prototype.__proto__) // null |
属性问题
- 读取对象的属性值时: 会自动到原型链中查找
- 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
- 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
- 代码示例
1 | function Fn() { } |
instanceof
- instanceof是如何判断的?
- 表达式:
A instanceof B
- 如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false
- Function是通过new自己产生的实例
1 | /* |
相关面试题
测试题1:
1 | /* |
测试题2:
1 | /* |
结果图例
执行上下文与执行上下文栈
当代码在 JavaScript 中运行时,执行代码的环境非常重要,并将概括为以下几点:
全局代码——第一次执行代码的默认环境。
函数代码——当执行流进入函数体时。
(…) —— 我们当作 执行上下文 是当前代码执行的一个环境与范围。
换句话说,当我们启动程序时,我们从全局执行上下文中开始。一些变量是在全局执行上下文中声明的。我们称之为全局变量。当程序调用一个函数时,会发生什么?
以下几个步骤:
- JavaScript 创建一个新的执行上下文,我们叫作本地执行上下文。
- 这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。
- 新的执行上下文被推到到执行堆栈中。可以将执行堆栈看作是一种保存程序在其执行中的位置的容器。
函数什么时候结束?当它遇到一个 return 语句或一个结束括号}。
当一个函数结束时,会发生以下情况:
- 这个本地执行上下文从执行堆栈中弹出。
- 函数将返回值返回调用上下文。调用上下文是调用这个本地的执行上下文,它可以是全局执行上下文,也可以是另外一个本地的执行上下文。这取决于调用执行上下文来处理此时的返回值,返回的值可以是一个对象、一个数组、一个函数、一个布尔值等等,如果函数没有 return 语句,则返回 undefined。
- 这个本地执行上下文被销毁,销毁是很重要,这个本地执行上下文中声明的所有变量都将被删除,不在有变量,这个就是为什么 称为本地执行上下文中自有的变量。
变量提升与函数提升
- 变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
- 函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
- 引出一个问题: 变量提升和函数提升是如何产生的?
1 | /* |
执行上下文
- 代码分类(位置)
- 全局代码
- 函数(局部)代码
- 全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
- 函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
- 对局部数据进行预处理
- 形参变量=>赋值(实参)=>添加为执行上下文的属性
arguments
=>赋值(实参列表), 添加为执行上下文的属性- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
执行上下文栈
- 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)–>
所以栈底百分百是[window]
- 在函数执行上下文创建后, 将其添加到栈中(压栈)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后, 栈中只剩下window
上下文栈数==函数调用数+1
1 | //1. 进入全局执行上下文 |
此处用一个动态图来展示:
举个栗子:
1 | //栗子 |
相关面试题
函数提升优先级高于变量提升,且不会被变量声明覆盖,但是会被变量赋值覆盖
1 | /* |
作用域与作用域链
作用域
- 理解
- 就是一块”地盘”, 一个代码段所在的区域
- 它是静态的(相对于上下文对象), 在编写代码时就确定了
- 分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了) –>(java语言也有)
- 作用
- 隔离变量,不同作用域下同名变量不会有冲突
1 | /* //没块作用域 |
作用域与执行上下文的区别与联系
- 区别1:
- 全局作用域之外,每个函数都会创建自己的作用域,
作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
- 区别2:
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
- 联系:
- 执行上下文(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数使用域
作用域链
- 理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
- 查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
1 | var a = 1 |
相关面试题
① 作用域在函数定义时就已经确定了。而不是在函数调用时
作用域1:作用域在函数定义时就已经确定了。而不是在函数调用时
1 | var x = 10; |
② 对象变量不能产生局部作用域
1 | var fn = function () { |
闭包预备知识点梳理
举个栗子分析执行上下文
在讨论闭包之前,让我们看下下方的代码(建议先只看代码自己头脑风暴再看笔记中的描述),也算是对上面知识点的梳理回顾:
1 | 1: let a = 3 |
为了理解 JavaScript 引擎是如何工作的,让我们详细分析一下:
- 在第 1 行,我们在全局执行上下文中声明了一个新变量 a,并将赋值为 3。
- 接下来就变得棘手了,第 2 行到第 5 行实际上是在一起的。这里发生了什么?
- 我们在全局执行上下文中声明了一个名为
addTwo
的新变量,我们给它分配了什么? –>一个函数定义
。 - 两个括号{}之间的任何内容都被分配给
addTwo
,函数内部的代码没有被求值,没有被执行,只是存储在一个变量中以备将来使用
。
- 现在我们在第 6 行。
- 它看起来很简单,但是这里有很多东西需要拆开分析。首先,我们在全局执行上下文中声明一个新变量,并将其标记为[
b
],变量一经声明,其值即为 undefined
。 - 接下来,仍然在第 6 行,我们看到一个赋值操作符。我们准备给变量
b
赋一个新值,接下来我们看到一个函数被调用。当您看到一个变量后面跟着一个圆括号(…)时,这就是调用函数的信号
,接着,每个函数都返回一些东西(值、对象或 undefined),无论从函数返回什么,都将赋值给变量b
。
- 但是首先我们需要调用标记为
addTwo
的函数。JavaScript 将在其全局执行上下文内存中查找名为addTwo
的变量。噢,它找到了一个,它是在[步骤 2(或第 2 - 5 行)中定义
]的。变量[addTwo
]包含一个函数定义。
- 注意:
变量[a]作为参数传递给函数
。 - JavaScript 在全局执行上下文内存中搜索变量
a
,找到它,发现它的值是 3,并将数字 3 作为参数传递给函数,准备好执行函数。
- 现在执行上下文将切换,创建了一个新的本地执行上下文,我们将其命名为[“
addTwo 执行上下文
”,执行上下文被推送到调用堆栈上
。在 addTwo 执行上下文中,我们要做的第一件事是什么?
- 你可能会说,“在 addTwo 执行上下文中声明了一个新的变量 ret”,
这是不对的
。 正确的答案是
:我们需要先看函数的参数。在 addTwo 执行上下文中声明一个新的变量[x]
,因为值 3 是作为参数传递的,所以变量 x 被赋值为 3。- 下一步才是在 addTwo 执行上下文中声明一个新的变量
ret
。它的值被设置为 undefined(第三行)。
- 仍然是第 3 行,需要执行一个相加操作。
- 首先我们需要
x
的值,JavaScript 会寻找一个变量x
,它会首先在addTwo
执行上下文中寻找,找到了一个值为 3。第二个操作数是数字 2。两个相加结果为 5 就被分配给变量ret
。
- 第 4 行,我们返回变量
ret
的内容,在 addTwo 执行上下文中查找,找到值为 5,返回,函数结束。 - 第 4 - 5 行,函数结束。
addTwo 执行上下文被销毁
,变量x
和ret
被消去了,它们已经不存在了。addTwo 执行上下文从调用堆栈中弹出
,返回值返回给调用上下文,在这种情况下,调用上下文是全局执行上下文,因为函数addTwo
是从全局执行上下文调用的。
- 现在我们继续第 4 步的内容,返回值 5 被分配给变量
b
,此时实际上程序仍然在第 6 行
(盗梦空间既视感:dog:) - 在第 7 行,
b
的值 5 被打印到控制台了。
对于一个非常简单的程序,这是一个非常冗长的解释,我们甚至还没有涉及闭包。但肯定会涉及的,不过首先我们得绕一两个弯。
举个栗子分析词法作用域
这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript 的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined)。
1 | 1: let val1 = 2 |
下面列出向个步骤来解释一下(如果你已经熟悉了,请跳过):
- 在全局执行上下文中声明一个新的变量
val1
,并将其赋值为 2。 - 行 2 - 5,声明一个新的变量
multiplyThis
,并给它分配一个函数定义。 - 第六行,声明一个在全局执行上下文
multiplied
新变量。 - 从全局执行上下文内存中查找变量
multiplyThis
,并将其作为函数执行,传递数字 6 作为参数。
- 新函数调用(创建新执行上下文),创建一个新的
multiplyThis
函数执行上下文。 - 在
multiplyThis
执行上下文中,声明一个变量 n 并将其赋值为 6
–>声明后才会进入函数体内部执行
- 执行函数回到第 3 行。
- 在
multiplyThis
执行上下文中,声明一个变量ret
。 - 继续第 3 行。对两个操作数 n 和 val1 进行乘法运算.在
multiplyThis
执行上下文中查找变量n
。- 我们在步骤 6 中声明了它,它的内容是数字 6。在
multiplyThis
执行上下文中查找变量val1
。 multiplyThis
执行上下文没有一个标记为 val1 的变量。我们向调用上下文查找,调用上下文是全局执行上下文,在全局执行上下文中
寻找 [val1
]。哦,是的、在那儿,它在步骤 1 中定义,数值是 2。
- 我们在步骤 6 中声明了它,它的内容是数字 6。在
- 继续第 3 行。将两个操作数相乘并将其赋值给
ret
变量,6 * 2 = 12,ret 现在值为 12。
- 返回
ret
变量,销毁multiplyThis
执行上下文及其变量ret
和n
。变量val1
没有被销毁,因为它是全局执行上下文的一部分。 - 回到第 6 行。在调用上下文中,数字 12 赋值给
multiplied
的变量。 - 最后在第 7 行,我们在控制台中打印
multiplied
变量的值
在这个例子中,我们需要记住一个函数可以访问在它的调用上下文中定义的变量,这个就是词法作用域(Lexical scope)。
返回函数的函数[高阶函数
]
在第一个例子中,函数addTwo
返回一个数字。请记住,函数可以返回任何东西。让我们看一个返回函数的函数示例,因为这对于下方理解闭包非常重要。看栗子:
1 | 1: let val = 7 |
让我们回到分步分解:
- 第一行。我们在全局执行上下文中声明一个变量
val
并赋值为 7。 - 行 2 - 8。我们在全局执行上下文中声明了一个名为
createAdder
的变量,并为其分配了一个函数定义。
- 内部的第 3 至 7 行描述了上述函数定义,和以前一样,在这一点上,我们没有直接讨论这个函数。我们只是将函数定义存储到[
createAdder
]变量中。
- 第 9 行。
- 我们在全局执行上下文中声明了一个名为
adder
的新变量,暂时,值为 undefined - 我们看到括号(),我们需要执行或调用一个函数,查找全局执行上下文的内存并查找名为
createAdder
的变量,它是在步骤 2 中创建的。好吧,我们调用它。
- 调用函数时,执行到第 2 行。
- 创建一个新的
createAdder
执行上下文。我们可以在createAdder
的执行上下文中创建自有变量。js 引擎将createAdder
的上下文添加到调用堆栈。这个函数没有参数,让我们直接跳到它的主体部分
.
- 第 3 - 6 行(执行到主体函数中)。
- 我们有一个新的函数声明,我们在
createAdder
执行上下文中创建一个变量 addNumbers。这很重要,addnumber
只存在于createAdder
执行上下文中。我们将函数定义存储在名为addNumbers
的自有变量中。 - 在第 7 行,我们返回变量
addNumbers
的内容。js 引擎查找一个名为addNumbers
的变量并找到它,这是一个函数定义。好的,函数可以返回任何东西,包括函数定义。我们返addNumbers
的定义。第 4 行和第 5 行括号之间的内容构成该函数定义。
- [return addNumbers]时,
createAdder
执行上下文将被销毁。addNumbers
变量不再存在。但addNumbers
函数定义仍然存在,因为它返回并赋值给了 adder 变量
。
此处很重要
!!!此时的[adder=createAdder()
]实际上它的值是[addNumbers
]的函数定义而不是[createAdder
]了,adder现在是一个匿名函数–这里有点绕,要确定理解
- 第 10 行。我们在全局执行上下文中定义了一个新的变量
sum
,先赋值为 undefined;
- 接下来我们需要执行一个函数。哪个函数?
- 是名为
adder
变量中定义的函数。我们在全局执行上下文中查找它,果然找到了它,这个函数有两个参数。 - 让我们查找这两个参数,第一个是我们在步骤 1 中定义的变量
val
,它表示数字 7,第二个是数字 8。
- 是名为
- 现在我们要执行这个函数,函数定义概述在第 3-5 行,
因为这个函数是匿名
,为了方便理解,我们暂且叫它adder
吧。这时创建一个adder
函数执行上下文,在adder
执行上下文中创建了两个新变量a
和b
。它们分别被赋值为 7 和 8,因为这些是我们在上一步传递给函数的参数。
- 执行回到第 4 行。
- 在
adder
执行上下文中声明了一个名为ret
的新变量, - 将变量
a
的内容和变量b
的内容相加得 15 并赋给 ret 变量。
ret
变量从该函数返回。这个匿名函数执行上下文被销毁,从调用堆栈中删除,变量a
、b
和ret
不再存在。- 返回值被分配给我们在步骤 9 中定义的
sum
变量。 - 我们将
sum
的值打印到控制台。
如预期,控制台将打印 15。我们在这里确实经历了很多困难,我想在这里说明几点。首先,函数定义可以存储在变量中,函数定义在程序调用之前是不可见的。其次,每次调用函数时,都会(临时)创建一个本地执行上下文。当函数完成时,执行上下文将消失。函数在遇到 return 或右括号}时执行完成。
高阶函数是什么?
所谓高阶函数,就是一个函数就可以接收另一个函数作为参数,或者是返回一个函数–>常见的高阶函数有map、reduce、filter、sort等
1 | var ADD =function add(a) { |
- map
1 | //map接受一个函数作为参数,不改变原来的数组,只是返回一个全新的数组 |
- reduce
1 | //reduce也是返回一个全新的数组。reduce接受一个函数作为参数,这个函数要有两个形参,代表数组中的前两项,reduce会将这个函数的结果与数组中的第三项再次组成这个函数的两个形参以此类推进行累积操作 |
- filter
1 | //filter返回过滤后的数组。filter也接收一个函数作为参数,这个函数将作用于数组中的每个元素,根据该函数每次执行后返回的布尔值来保留结果,如果是true就保留,如果是false就过滤掉(这点与map要区分) |
闭包
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
引出闭包概念
① 错误场景
需求: 点击某个按钮, 提示"点击的是第n个按钮"
1 | <button>测试1</button> |
此处错误是,直接修改并使用全局变量[i
],导致for循环结束后,所有点击按钮绑定的弹窗值都是[i+1
]
随后调用时,都会找到[i
]这个变量,但是此时i==3,所以所有结果都是4
② 将变量挂载到自身来解决
解决方式:将btn所对应的下标保存在btn上
1 | <button>测试1</button> |
将其放在自己的身上,需要时自己找自己拿,这样就能解决
③ 利用闭包
1 | <body> |
举个闭包栗子分析理解
① 按照正常逻辑理解
先说,此部分不是按照闭包机制进行理解,所以中途发觉不对(肯定有地方不对劲
)请勿钻牛角尖,主要用作后方对照
1 | 1: function createCounter() { |
现在,我们已经从前几个示例中掌握了它的诀窍,让我们按照预期的方式快速执行它: (错误的流程理解,故意按照正常的逻辑流程走,做印证
)
- 行 1 - 8。我们在全局执行上下文中创建了一个新的变量
createCounter
,并赋值了一个的函数定义。 - 第 9 行。
- 我们在全局执行上下文中声明了一个名为
increment
的新变量。 - 我们需要调用
createCounter
函数并将其返回值赋给increment
变量。
- 返回执行 行 1 - 8。调用函数,创建新的本地执行上下文。
- 第 2 行。在本地执行上下文中,声明一个名为
counter
的新变量并赋值为 0; - 行 3 - 6。声明一个名为
myFunction
的新变量,变量在本地执行上下文中声明,变量的内容是为第 4 行和第 5 行所定义。 - 第 7 行。返回
myFunction
变量的内容,删除本地执行上下文。变量myFunction
和counter
不再存在。此时控制权回到了调用上下文。
- 再次回到 第 9 行
- 在调用上下文(全局执行上下文)中,
createCounter
返回的值赋给了increment
,变量increment
现在包含一个函数定义内容为createCounter
返回的函数。 - 它不再标记为
myFunction
,但它的定义是相同的。在全局上下文中,它是的标记为labeledincrement
。
- 第 10 行。声明一个新变量(c1)。
- 继续第 10 行。查找
increment
变量,它是一个函数并调用它。它包含前面返回的函数定义,如第 4-5 行所定义的。 - 创建一个新的执行上下文。没有参数。开始执行函数。
- 回到 第 4 行。
- counter=counter + 1。在本地执行上下文中查找
counter
变量。 - 我们只是创建了那个上下文,从来没有声明任何局部变量。让我们看看全局执行上下文。这里也没有
counter
变量。 - Javascript 会将其计算为 counter = undefined + 1,声明一个标记为
counter
的新局部变量,并将其赋值为 number 1,因为 undefined 被当作值为 0。
–>此处是错误的哦,别钻牛角尖,正确的理解在下方,此处是做错误对比
- 第 5 行。我们变量
counter
的值(1),我们销毁本地执行上下文和counter
变量。 - 回到第 10 行。返回值(1)被赋给 c1。
- 第 11 行。重复步骤 10-14,c2 也被赋值为 1。
- 第 12 行。重复步骤 10-14,c3 也被赋值为 1。
- 第 13 行。我们打印变量 c1 c2 和 c3 的内容。
你自己试试,看看会发生什么。你会将注意到,它并不像从我上面的解释中所期望的那样记录 1,1,1。而是记录 1,2,3
。这个是为什么?
②正确的理解
不知怎么滴,increment
函数记住了那个cunter
的值。这是怎么回事?
- counter是全局执行上下文的一部分吗?
- 尝试 console.log(counter),得到undefined的结果,显然不是这样的。
- 也许,当你调用increment时,它会以某种方式返回它创建的函数(createCounter)?
- 这怎么可能呢?变量increment包含函数定义,而不是函数的来源,显然也不是这样的。
- 所以一定有另一种机制。闭包,我们终于找到了,丢失的那块。
- **-
它是这样工作的,无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变
**
所以我们上面的解释都是错的,让我们再试一次,但是这次是正确的
1 | 1: function createCounter() { |
- 同上,第
1-8
行。我们在全局执行上下文中创建了一个新的变量createCounter
,它得到了指定的函数定义。 - 同上,第
9
行。
- 我们在全局执行上下文中声明了一个名为
increment
的新变量。 - 我们需要调用
createCounter
函数并将其返回值赋给increment
变量。
- 同上,第
1-8
行。调用函数,创建新的本地执行上下文。
- 第
2
行。在本地执行上下文中,声明一个名为counter
的新变量并赋值为0
。 - 第
3-6
行。声明一个名为myFunction
的新变量,变量在本地执行上下文中声明,变量的内容是另一个函数定义。如第4
行和第5
行所定义,现在我们还创建了一个闭包,并将其作为函数定义的一部分。闭包包含作用域中的变量,在本例中是变量counter
(值为0
)。 - 第
7
行。返回myFunction
变量的内容,删除本地执行上下文。myFunction
和counter
不再存在。控制权交给了调用上下文,我们返回函数定义和它的闭包,闭包中包含了创建它时在作用域内的变量。
- 回到第
9
行。
- 在调用上下文(全局执行上下文)中,
createCounter
返回的值被指定为increment
- 变量
increment
现在包含一个函数定义(和闭包),由createCounter返回的函数定义,它不再标记为myFunction
,但它的定义是相同的,在全局上下文中,称为increment
。
- 第
10
行。声明一个新变量c1
。
- 继续第
10
行。查找变量increment
,它是一个函数,调用它。它包含前面返回的函数定义,如第4-5
行所定义的。(它还有一个带有变量的闭包
)。 - 创建一个新的执行上下文,没有参数,开始执行函数。
- 第
4
行。[counter = counter + 1
],寻找变量 [counter
],在查找本地或全局执行上下文之前,让我们检查一下闭包
,瞧,闭包包含一个名为[counter
]的变量,其值为0
。在第4
行表达式之后,它的值被设置为1
。它再次被储存在闭包里,闭包现在包含值为1
的变量 [counter
]。 - 第
5
行。我们返回counter的值
,销毁本地执行上下文。 - 回到第
10
行。返回值1
被赋给变量c1
。 - 第
11
行。我们重复步骤10-14
。这一次,在闭包中此时变量counter
的值是1。它在第12
行设置的,它的值被递增并以2
的形式存储在递增函数的闭包中,c2
被赋值为2
。 - 第
12
行。重复步骤10-14
行,c3
被赋值为3。 - 第13行。我们打印变量
c1 c2
和c3
的值。
** 你此时可能会问,是否有任何函数具有闭包,甚至是在全局范围内创建的函数?
**
答案是肯定的。在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。
但当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。
常见的闭包
① 将函数作为另一个函数的返回值
1 | // 1. 将函数作为另一个函数的返回值 |
② 将函数作为实参传递给另一个函数调用
1 | // 2. 将函数作为实参传递给另一个函数调用 |
③ 高阶函数与柯里化
1、从 ES6 高阶箭头函数理解函数柯里化(运用到闭包
)
- 首先看到了这样的一个例子:
1 | let add = a => b => a + b |
- 以上是一个很简单的相加函数,把它转化成 ES5 的写法如下
1 | function add(a) { |
- 再简化一下,可以写成如下形式:
1 | let add = function(a) { |
- 虽然好像没什么意义,但是很显然上述使用了[
闭包
],而且该函数的返回值是一个函数。其实,这就是高阶函数的定义:以函数为参数或者返回值是函数的函数。
2、柯里化
- 图例:
- 关键就是
理解柯里化
,其实可以把它理解成,柯里化后,将第一个参数变量存在函数里面了(闭包)
,然后本来需要n个参数的函数可以变成只需要剩下的(n - 1个)参数就可以调用,比如
1 | let add = x => y => x + y |
4、总结
- 如果是
a => b => c => {xxx}
这种多次柯里化的,如何理解?
理解:前n - 1
次调用,其实是提前将参数传递进去,并没有调用最内层函数体,最后一次调用才会调用最内层函数体,并返回最内层函数体的返回值
结合上文可知,这里的多个连续箭头(无论俩个箭头函数三个及以上)函数连在一起 就是在柯里化。所以连续箭头函数就是多次柯里化函数的 es6 写法。
调用特点
:let test = a => b => c => {xxx}
比如对于上面的 test
函数,它有 3 个箭头, 这个函数要被调用 3 次 test(a)(b)(c)
,前两次调用只是在传递参数,只有最后依次调用才会返回 {xxx}
代码段的返回值,并且在 {xxx}
代码段中可以调用 a,b,c
闭包的作用
- 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
- 函数执行完后, 函数内部声明的局部变量是否还存在?
- 一般是不存在, 存在于闭包中的变量才可能存在
- 在函数外部能直接访问函数内部的局部变量吗?
- 不能, 但我们可以通过闭包让外部操作它
闭包的生命周期
- 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
- 死亡: 在嵌套的内部函数成为垃圾对象时
- 即没有人指向它时死亡,通常置为[
null
],当然指向其他也行,但不安全(容易污染变量)
1 | //闭包的生命周期 |
闭包的应用
闭包的应用 : 定义JS模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包含n个方法的对象或函数
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
- 模块定义:
- ```js
//myModule.js
function myModule() {
//私有数据
var msg = ‘My atguigu’
//操作数据的函数
function doSomething() {
}console.log('doSomething() '+msg.toUpperCase())
function doOtherthing () {
} //向外暴露对象(给外部使用的方法)console.log('doOtherthing() '+msg.toLowerCase())
return {
}doSomething: doSomething, doOtherthing: doOtherthing
}
// myModule2.js
(function () {
//私有数据
var msg = ‘My atguigu’
//操作数据的函数
function doSomething() {
}console.log('doSomething() '+msg.toUpperCase())
function doOtherthing () {
} //向外暴露对象(给外部使用的方法)console.log('doOtherthing() '+msg.toLowerCase())
window.myModule2 = {
}doSomething: doSomething, doOtherthing: doOtherthing
})()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2. 模块调用
- ```js
//调用示例
------------ 模块调用1 --------------------------------------------
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule()
module.doSomething()
module.doOtherthing()
</script>
------------ 模块调用2 --------------------------------------------
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
myModule2.doSomething()
myModule2.doOtherthing()
</script>
闭包的缺点及解决
- 缺点:
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 容易造成内存泄露
- 解决:
能不用闭包就不用
及时释放
```js
function fn1() {
var arr = new Array(100000)
function fn2() {console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
f = null //让内部函数成为垃圾对象–>回收闭包1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
我还有一个解决方式,调用时直接`f()()`直接运行调用即可-->匿名函数,用完自动就销毁了
[![4dRzFK.png](https://z3.ax1x.com/2021/09/23/4dRzFK.png)](https://imgtu.com/i/4dRzFK)
### 内存溢出与内存泄露
1. 内存溢出
* 一种程序运行出现的错误
* 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
2. 内存泄露
* 占用的内存没有及时释放
* `内存泄露积累多了就容易导致内存溢出`
* 常见的内存泄露:
* 意外的全局变量
* 没有及时清理的计时器或回调函数
* 闭包
```js
<script type="text/javascript">
// 1. 内存溢出
var obj = {}
for (var i = 0; i < 10000; i++) {
obj[i] = new Array(10000000)
console.log('-----')
}
// 2. 内存泄露
// 意外的全局变量
function fn() {
a = new Array(10000000) //不使用var let const去承接
console.log(a)
}
fn()
// 没有及时清理的计时器或回调函数
var intervalId = setInterval(function () { //启动循环定时器后不清理
console.log('----')
}, 1000)
// clearInterval(intervalId)
// 闭包
function fn1() {
var a = 4
function fn2() {
console.log(++a)
}
return fn2
}
var f = fn1()
f()
// f = null
</script>
不使用let const var等去声明,实际上是挂载到[window
]上的,所以导致内存泄露
相关面试题1
1 | //代码片段一 -->没有产生闭包:因为内部函数没有调用外部变量 |
- 代码片段一:
- 函数体的
this
指向是[window
] - 没有产生闭包:因为内部函数没有调用外部变量
- 代码片段二为何指向是对象?
- this指向是调用它的[
getNameFunc
],他是对象中的属性,所以this指向就是object - 产生了闭包
相关面试题2
1 | function fun(n,o) { |
面向对象高级
对象创建模式
Object构造函数模式
- 套路: 先创建空Object对象, 再动态添加属性/方法
- 适用场景: 起始时不确定对象内部数据
- 问题: 语句太多
1 | /*一个人: name:"Tom", age: 12*/ |
对象字面量模式
- 套路: 使用{}创建对象, 同时指定属性/方法
- 适用场景: 起始时对象内部数据是确定的
- 问题: 如果创建多个对象, 有重复代码
1 | //对象字面量模式 |
工厂模式
- 套路: 通过工厂函数动态创建对象并返回
- 适用场景: 需要创建多个对象
- 问题:
对象没有一个具体的类型
, 都是Object类型
1 | //返回一个对象的函数===>工厂函数 |
自定义构造函数模式
- 套路: 自定义构造函数, 通过new创建对象
- 适用场景: 需要创建多个
类型确定
的对象,与上方工厂模式有所对比 - 问题: 每个对象都有相同的数据, 浪费内存
1 | //定义类型 |
构造函数+原型的组合模式
最好用这个写法
- 套路: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上
- 适用场景: 需要
创建多个类型确定
的对象 - 放在原型上可以节省空间(只需要加载一遍方法)
1 | //在构造函数中只初始化一般函数 |
继承模式
原型链继承
- 套路
- 定义父类型构造函数
- 给父类型的原型添加方法
- 定义子类型的构造函数
- 创建父类型的对象赋值给子类型的原型
将子类型原型的构造属性设置为子类型
–>此处有疑惑的可以看本笔记[函数高级部分的1、原型与原型链](#1、原型与原型- 给子类型原型添加方法
- 创建子类型的对象: 可以调用父类型的方法
- 关键
子类型的原型为父类型的一个实例对象
1 | //父类型 |
① 示例图
注意
:此图中没有体现[constructor构造函数
],会在下方构造函数补充处指出
② 构造函数补充
对于代码中[Sub.prototype.constructor = Sub
]是否有疑惑?
如果不加,其构造函数找的[new Supper()
]是从顶层Object继承来的构造函数,指向[Supper()
],虽然如果你不加这句话,大体上使用是不受影响的,但是你有一个属性指向是错误的,如果在大型项目中万一万一哪里再调用到了呢?
- 这里可以补充一下constructor 的概念:
constructor 我们称为构造函数,因为它指回构造函数本身
- 其作用是让某个构造函数产生的 所有实例对象(比如f) 能够找到他的构造函数(比如Fun),用法就是f.constructor
- 此时实例对象里没有constructor 这个属性,于是沿着原型链往上找到Fun.prototype 里的constructor,并指向Fun 函数本身
- constructor本就存在于原型中,指向构造函数,成为子对象后,如果该原型链中的constructor在自身没有而是在父原型中找到,所以指向父类的构造函数
- 由于这里的继承是直接改了构造函数的prototype 的指向,所以在 sub的原型链中,Sub.prototype 没有constructor 属性,反而是看到了一个super 实例
- 这就让sub 实例的constructor 无法使用了。为了他还能用,就在那个super 实例中手动加了一个constructor 属性,且指向Sub 函数看到了一个super 实例
借用构造函数继承(假的)
- 套路:
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造
- 关键:
在子类型构造函数中通用call()调用父类型构造函数
- 作用:
- 能借用父类中的构造方法,但是不灵活
1 | function Person(name, age) { |
[Person
]中的this是动态变化的,在[Student
]中利用[Person.call(this, name, age)
]改变了其this指向,所以可以实现此效果
组合继承
方式3: 原型链+借用构造函数的组合继承
- 利用原型链实现对父类型对象的方法继承
- 利用call()借用父类型构建函数初始化相同属性
1 | function Person(name, age) { |
线程机制与事件机制
进程与线程
进程
- 程序的一次执行,它
占有一片独有的内存空间
- 可以通过windows任务管理器查看进程
线程
概念:
- 是进程内的一个独立执行单元
- 是程序执行的一个完整流程
- 是CPU的最小的调度单元
进程与线程
- 应用程序必须运行在某个进程的某个线程上
- 一个进程中至少有一个运行的线程:主线程 ,进程启动后自动创建
- 一个进程中也可以同时运行多个线程:此时我们会说这个程序是多线程运行的
- 多个进程之间的数据是不能直接共享的,内存相互独立(隔离)
线程池(thread pool)
:保存多个线程对象的容器,实现线程对象的反复利用
引出的问题
① 何为多进程与多线程?
多进程运行: 一应用程序可以同时启动多个实例运行
多线程: 在一个进程内, 同时有多个线程运行
②比较单线程与多线程?
多线程:
- 优点:
- 能有效提升CPU的利用率
- 缺点
- 创建多线程开销
- 线程间切换开销
- 死锁与状态同步问题
单线程:
- 优点:顺序编程简单易懂
- 缺点:效率低
③ JS是单线程还是多线程?
JS是单线程运行的 , 但使用H5中的 Web Workers可以多线程运行
- 只能由一个线程去操作DOM界面
④ 浏览器运行是单线程还是多线程?
都是多线程运行的
⑤ 浏览器运行是单进程还是多进程?
有的是单进程:
- firefox
- 老版IE
有的是多进程:
- chrome
- 新版IE
如何查看浏览器是否是多进程运行的呢? 任务管理器–>进程
浏览器内核
支撑浏览器运行的最核心的程序
不同浏览器的内核
- Chrome, Safari : webkit
- firefox : Gecko
- IE : Trident
- 360,搜狗等国内浏览器: Trident + webkit
内核由什么模块组成?
主线程
- js引擎模块 : 负责js程序的编译与运行
- html,css文档解析模块 : 负责页面文本的解析(拆解)
- dom/css模块 : 负责dom/css在内存中的相关处理
- 布局和渲染模块 : 负责页面的布局和效果的绘制
- 布局和渲染模块 : 负责页面的布局和效果的绘制
分线程
- 定时器模块 : 负责定时器的管理
- 网络请求模块 : 负责服务器请求(常规/Ajax)
- 事件响应模块 : 负责事件的管理
图例
定时器引发的思考
1 | <body> |
定时器真是定时执行的吗?
- 定时器并不能保证真正定时执行
- 一般会延迟一丁点(可以接受), 也有可能延迟很长时间(不能接受)
定时器回调函数是在分线程执行的吗?
在主线程执行的, JS是单线程的
定时器是如何实现的?
事件循环模型
JS是单线程的
如何证明JS执行是单线程的
setTimeout()的回调函数是在主线程执行的
- 定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行
1 | // 如何证明JS执行是单线程的 |
流程结果:
- 先打印了[
fn()
],然后马上就打印了[timeout() 00000
] - 过了一秒后 打印 timeout 1111并弹窗,此处如果不将弹窗关闭,不会继续执行上方222
- 在将[timeout 1111]弹窗关闭后,
再等一秒
执行此处
- 问:为何明明写的是2秒,却关闭上一个弹窗再过一秒就执行?
- 解:并不是关闭后再计算的,而是一起计算的,alert只是暂停了主线程执行
JS引擎执行代码的基本流程与代码分类
代码分类:
- 初始化代码
- 回调代码
js引擎执行代码的基本流程
- 先执行初始化代码: 包含一些特别的代码 回调函数(异步执行)
- 设置定时器
- 绑定事件监听
- 发送ajax请求
- 后面在某个时刻才会执行回调代码
为什么js要用单线程模式, 而不用多线程模式?
JavaScript的单线程,与它的用途有关。
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。
这决定了它只能是单线程,否则会带来很复杂的同步问题
- 举个栗子:如果我们要实现更新页面上一个dom节点然后删除,用单线程是没问题的
- 但是如果多线程,当我删除线程先删除了dom节点,更新线程要去更新的时候就会出错
事件循环模型(Event Loop)机制
概念引出
我们都知道,javascript从诞生之日起就是一门单线程的非阻塞的脚本语言
。这是由其最初的用途来决定的:与浏览器交互
。
单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。
非阻塞
:
而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。
单线程是必要的
:
也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。
当然,现如今人们也意识到,单线程在保证了执行顺序的同时也限制了javascript的效率,因此开发出了web workers
技术。这项技术号称可以让javaScript成为一门多线程语言。
然而,使用web workers技术开的多线程有着诸多限制,例如:所有新线程都受主线程的完全控制,不能独立执行
。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行I/O操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了javascript语言的单线程本质。
可以预见,未来的javascript也会一直是一门单线程的语言。
话说回来,前面提到javascript的另一个特点是“非阻塞
”,那么javascript引擎到底是如何实现的这一点呢?
答案就是——event loop(事件循环)。
注:虽然nodejs中的也存在与传统浏览器环境下的相似的事件循环。然而两者间却有着诸多不同,故把两者分开,单独解释
。
浏览器环境下JS引擎的事件循环机制
① 执行栈概念
当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)
和栈(stack)
中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。 但是我们这里说的执行栈和上面这个栈的意义却有些不同
。
执行栈
:
当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫
执行上下文
。这个执行环境中存在着这个方法的私有作用域、上层作用域的指向、方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境
。这个过程反复进行,直到执行栈中的代码全部执行完毕。
此处继续拿出栈图加深理解:
从图片可知,一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值
。
以上的过程说的都是同步代码的执行。那么当一个异步代码(如发送ajax请求数据)执行后会如何呢?
刚刚说过js的另一大特点是非阻塞,实现这一点的关键在于下面要说的这项机制——
事件队列(Task Queue)
。
② 事件队列(Task Queue)
JS引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务,当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列
。
被放入事件队列不会立刻执行其回调,而是
等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务
。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
这里还有一张图来展示这个过程:
图中的stack表示我们所说的执行栈,web apis则是代表一些异步事件,而callback queue即事件队列。
以上的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)
。
宏任务(macro task)与微任务(micro task)
① 宏任务队列与微任务队列解释
顾名思义,宏任务放至宏任务队列(简称宏队列
)中、微任务放至微任务队列(简称微队列
)中
- JS中用来存储待执行回调函数的队列包含2个不同特定的列队
宏队列
:用来保存待执行的宏任务(回调),比如:定时器
回调/ajax回调/dom事件回调微队列
:用来保存待执行的微任务(回调),比如:Promise
的回调/muntation回调
- JS执行时会区别这2个队列:
- JS执行引擎首先必须执行所有的
初始化同步任务
代码 - 每次准备取出第一个
宏任务执行前
,都要将所有的微任务
一个一个取出来执行
- JS执行引擎首先必须执行所有的
前面我们介绍过,在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。
我们只需记住:** 当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行
**
② 原理图
③ 由代码逆向理解宏任务与微任务
代码示例
1 | setTimeout(() => { |
结果
1 | '同步代码', |
node环境下 的事件循环机制
不学node的小伙伴就跳过此部分直接去下一节Web Workers笔记吧
① 与浏览器环境有何不同?
在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中
。
② 事件循环模型
下面是一个libuv引擎中的事件循环的模型:
1 | //libuv引擎中的事件循环的模型 |
注:模型中的每一个方块代表事件循环的一个阶段
这个模型是node官网上的一篇文章中给出的,我下面的解释也都来源于这篇文章。我会在文末把文章地址贴出来,有兴趣的朋友可以亲自与看看原文。
③ 事件循环各阶段详解
从上面这个模型中,我们可以大致分析出node中的事件循环的顺序:
外部输入数据–>轮询阶段(poll)–>检查阶段(check)–>关闭事件回调阶段(close callback)–>定时器检测阶段(timer)–>I/O事件回调阶段(I/O callbacks)–>闲置阶段(idle, prepare)–>轮询阶段…
这些阶段大致的功能如下:
- timers(定时器检测阶段): 这个阶段执行定时器队列中的回调如
setTimeout()
和setInterval()
。 - I/O callbacks(I/O事件回调阶段): 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和
setImmediate()
的回调。 - idle, prepare: 这个阶段仅在内部使用,可以不必理会。
- poll(轮询阶段): 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- check(检查阶段):
setImmediate()
的回调会在这个阶段执行。 - close callbacks(关闭事件回调阶段): 例如
socket.on('close', ...)
这种close事件的回调。
下面我们来按照代码第一次进入libuv引擎后的顺序来详细解说这些阶段:
poll(轮询阶段)
当个v8引擎将js代码解析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,收到代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback。
值得注意的是,poll阶段在执行poll queue中的回调时实际上不会无限的执行下去。有两种情况poll阶段会终止执行poll queue中的下一个回调:1.所有回调执行完毕。2.执行数超过了node的限制。
check(检查阶段)
check阶段专门用来执行setImmediate()
方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段。
close callbacks(关闭事件回调阶段)
当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()
方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()
方法发送出去。
timers(定时器检测阶段)
这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数。
I/O callbacks(I/O事件回调阶段)
如上文所言,这个阶段主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。
④ process.nextTick,setTimeout与setImmediate的区别与使用场景
在node中有三个常用的用来推迟任务执行的方法:process.nextTick,setTimeout(setInterval与之相同)与setImmediate
这三者间存在着一些非常不同的区别:
process.nextTick()
尽管没有提及,但是实际上node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。与执行poll queue中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用process.nextTick()
方法会导致node进入一个死循环。。直到内存泄漏。
使用这个方法比较合适呢?下面有一个例子:
1 | const server = net.createServer(() => {}).listen(8080); |
这个例子中当,当listen方法被调用时,除非端口被占用,否则会立刻绑定在对应的端口上。这意味着此时这个端口可以立刻触发listening事件并执行其回调。然而,这时候on('listening)
还没有将callback设置好,自然没有callback可以执行。为了避免出现这种情况,node会在listen事件中使用process.nextTick()
方法,确保事件在回调函数绑定后被触发。
setTimeout()和setImmediate()
在三个方法中,这两个方法最容易被弄混。实际上,某些情况下这两个方法的表现也非常相似。然而实际上,这两个方法的意义却大为不同。
setTimeout()
方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。注意这个“第一时间执行”,这意味着,受到操作系统和当前执行任务的诸多影响,该回调并不会在我们预期的时间间隔后精准的执行。执行的时间存在一定的延迟和误差,这是不可避免的。node会在可以执行timer回调的第一时间去执行你所设定的任务。
setImmediate()
方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。有趣的是,这个名字的意义和之前提到过的process.nextTick()
方法才是最匹配的。node的开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来—因为有大量的node程序使用着这两个方法,调换命名所带来的好处与它的影响相比不值一提。
setTimeout()
和不设置时间间隔的setImmediate()
表现上及其相似。猜猜下面这段代码的结果是什么?
1 | setTimeout(() => { |
实际上,答案是不一定。没错,就连node的开发者都无法准确的判断这两者的顺序谁前谁后。这取决于这段代码的运行环境。运行环境中的各种复杂的情况会导致在同步队列里两个方法的顺序随机决定。但是,在一种情况下可以准确判断两个方法回调的执行顺序,那就是在一个I/O事件的回调中。下面这段代码的顺序永远是固定的:
1 | const fs = require('fs'); |
答案永远是:
1 | immediate |
因为在I/O事件的回调中,setImmediate方法的回调永远在timer的回调前执行。
Web Workers
想了解更多可以点击链接查看更多,此处只是大致了解学习
- H5规范提供了js分线程的实现, 取名为: Web Workers
- 相关API
- Worker: 构造函数, 加载分线程执行的js文件
- Worker.prototype.onmessage: 用于接收另一个线程的回调函数
- Worker.prototype.postMessage: 向另一个线程发送消息
- 不足
- worker内代码不能操作DOM(更新UI)
- 不能跨域加载JS
- 不是每个浏览器都支持这个新特性
抛砖引玉,引出用处
还是拿斐波那契(Fibonacci)数列来做例子,这东西效率低,可以拿来模拟
1 | <body> |
当我运行此行代码,传入计算数值为50左右(有的甚至更低),整个页面就会卡住好久的时间不能操作(计算结束后才会弹窗,但是未弹窗的这段时间用户并不能进行操作),这时候就会发现单线程的弊端了
尝试使用
H5规范提供了js分线程的实现, 取名为:
Web Workers
相关API
Worker
: 构造函数, 加载分线程执行的js文件Worker.prototype.onmessage
: 用于接收另一个线程的回调函数Worker.prototype.postMessage
: 向另一个线程发送消息
- 不足
- worker内代码不能操作DOM(更新UI)
- 不能跨域加载JS
- 不是每个浏览器都支持这个新特性
① 主线程
创建一个Worker对象
绑定[主线程接收分线程返回的数据]方法
主线程向分线程发送数据,然后等待接受数据
接收到分线程回馈的数据,将数据进行处理(如弹窗)
1 | <body> |
② 分线程
将计算放置分线程中
注意
:alert(result) alert是window的方法, 在分线程不能调用,分线程中的全局对象不再是window
, 所以在分线程中不可能更新界面
1 | //worker.js |