JavaScript高级篇
# 基础总结深入
# 1. 数据类型
# 1.1 分类
1.基本(值)类型
类型 | 可能的值 |
---|---|
String | 任意字符串 |
Number | 任意的数字 |
boolean | true/false |
undefined | undefined |
null | null |
2.对象(引用)类型
类型 | 特征 |
---|---|
Object | 任意对象 |
Function | 一种特别的对象(可以执行) |
Array | 一种特别的对象(数值下标, 内部数据是有序的) |
# 1.2 类型的判断
关键字 | 作用 |
---|---|
typeof | 返回数据类型的字符串表达 |
instanceof | 判断对象的具体类型,即判断某个实例是否为特定的类型,并将判断结果的布尔值返回 |
=== | 严格的比较判断,不做类型转换,类型不同的一定不等 |
关键字 | 可以判断 | 不能区分 |
---|---|---|
typeof | undefined/ 数值 / 字符串 / 布尔值 / function | null与object 、object与array |
instanceof | 实例 | |
=== | undefined, null |
例子
<script type="text/javascript">
//1. 基本
// typeof返回数据类型的字符串表达
var a
console.log(a, typeof a, typeof a==='undefined',a===undefined ) // undefined 'undefined' true true
console.log(undefined==='undefined')
a = 4
console.log(typeof a==='number')
a = 'atguigu'
console.log(typeof a==='string')
a = true
console.log(typeof a==='boolean')
a = null
console.log(typeof a, a===null) // 'object'
console.log('-----------------')
//2. 对象
var b1 = {
b2: [1, 'abc', console.log],
b3: function () {
console.log('b3')
return function () {
return 'xfzhang'
}
}
}
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
console.log(typeof b1.b2, '-------') // 'object'
console.log(typeof b1.b3==='function') // true
console.log(typeof b1.b2[2]==='function')
b1.b2[2](4)
console.log(b1.b3()())
</script>
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
# 1.3 相关问题
1.undefined与null的区别?
- undefined代表定义未赋值
- null定义并赋值了, 只是值为null
2.什么时候给变量赋值为null呢?
- 初始赋值, 表明将要赋值为对象
- 结束前, 让对象成为垃圾对象(被垃圾回收器回收)
3.严格区别变量类型与数据类型?
数据的类型:①基本类型②对象类型
变量的类型(变量内存值的类型)
- 基本类型: 保存就是基本类型的数据
- 引用类型: 保存的是地址值
# 2. 数据-变量-内存
# 2.1 什么是数据?
- 存储在内存中代表特定信息的事务, 本质上是0101...
- 数据的特点: 可传递, 可运算
- 一切皆数据
- 内存中所有操作的目标: 数据
JS中运算的分类
- 算术运算
- 逻辑运算
- 赋值
- 运行函数
# 2.2 什么是内存?
答:内存条通电后产生的可储存数据的空间(临时的)
内存产生和死亡过程
内存条(电路版)== >通电== >产生内存空间== >存储数据== >处理数据== >断电==>内存空间和数据都消失
应用程序中分配给变量一小块内存,它们都在栈内存中,分为:
- 内部存储的数据
- 地址值
内存分类
- 栈: 全局变量/局部变量
- 堆: 对象
# 2.3 什么是变量?
1. 可变化的量, 由变量名和变量值组成
2. 每个变量都对应的一块小内存, 变量名用来查找对应的内存, 变量值就是内存中保存的数据
# 2.4 内存,数据, 变量三者之间的关系
1. 内存是用来存储数据的空间
2. 变量是内存的标识
# 2.5 赋值与内存的问题
var a = xxx, a内存中到底保存的是什么?
- xxx是基本(类型)数据, 保存的就是这个数据
- xxx是对象, 保存的是对象的地址值
- xxx是一个变量, 保存的xxx的内存内容(可能是基本数据, 也可能是地址值)
# 2.6 关于引用变量赋值问题
- 多个引用变量指向同一个对象, 通过一个变量修改对象内部数据, 另外的变量看到的是修改之后的数据
- 多个引用变量指向同一个对象, 让其中一个引用变量指向另一个对象, 另外的引用变量依然指向前一个对象
# 2.7 关于数据传递问题
在js调用函数时传递变量参数时, 是值传递还是引用传递?
- 理解1: 都是值(基本/地址值)传递
- 理解2: 可能是值传递, 也可能是引用传递(地址值)
# 2.8 JS引擎如何管理内存?
内存生命周期
1. 分配给变量小块内存空间, 并得到它的使用权
2. 存储数据, 可以反复进行操作
3. 使用完后释放相应的内存空间
释放内存
1. 局部变量: 函数执行完自动释放
2. 对象: 成为垃圾对象==>垃圾回收器回收
# 3. 对象
# 3.1 什么是对象?
1. 多个数据的封装体
2. 用来保存多个数据的容器
3. 一个对象代表现实中的一个事物
# 3.2 为什么要用对象?
- 统一管理多个数据
# 3.3 对象的组成
1. 属性: 属性名(字符串)和属性值(任意)组成
2. 方法: 一种特别的属性(属性值是函数)
# 3.4 如何访问对象内部数据?
1. .属性名: 编码简单, 有时不能用
2. ['属性名']: 编码麻烦, 能通用
# 3.5 相关问题
问题: 什么时候必须使用['属性名']的方式?
- 属性名包含特殊字符,如: - 空格
- 属性名不确定
示例
<script type="text/javascript">
var p = {}
//1. 给p对象添加一个属性: content type: text/json
// p.content-type = 'text/json' //不能用
p['content-type'] = 'text/json'
console.log(p['content-type'])
//2. 属性名不确定
var propName = 'myAge'
var value = 18
// p.propName = value //不能用
p[propName] = value
console.log(p[propName])
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4. 函数
# 4.1什么是函数?
- 实现特定功能的n条语句的封装体
- 只有函数是可以执行的, 其它类型的数据不能执行
# 4.2 为什么要用函数?
1. 提高代码复用
2. 便于阅读交流
# 4.3 如何定义函数?
1. 函数声明
2. 表达式
示例
//函数声明
function fn1 () {
console.log('fn1()')
}
//表达式
var fn2 = function () {
console.log('fn2()')
}
2
3
4
5
6
7
8
9
# 4.4 如何调用(执行)函数?
1. **test()**: 直接调用
2. **obj.test()**: 通过对象调用
3. **new test()**: new调用
4. **test.call/apply(obj)**: 临时让test成为obj的方法进行调用
示例
fn1()
fn2()
var obj = {}
function test2 () {
this.xxx = 'atguigu'
}
// obj.test2() 不能直接调用, obj对象根本就没有test2方法
test2.call(obj) // 相当于obj.test2() ,可以让一个函数成为指定任意对象的方法进行调用
console.log(obj.xxx)
2
3
4
5
6
7
8
9
10
# 5. 回调函数
# 5.1 什么函数才是回调函数?
- 你定义的
- 你没有调
- 但最终它执行了(在某个时刻或某个条件下)
# 5.2 常见回调函数及其从属的对象?
1. dom事件回调函数 ==>发生事件的dom元素
2. 定时器回调函数 ===>window
3. Ajax请求回调函数(后面讲)
4. 生命周期回调函数(后面讲)
# 6. IIFE(立即调用函数表达式)
IIFE全称 Immediately-Invoked Function Expression,别名: 匿名函数自调用(但不是十分准确的翻译)。是一种利用JavaScript函数生成新作用域的编程方法。
- IIFE可以令其函数中声明的变量绕过JavaScript的变量置顶声明规则
- 还可以避免新的变量被解释成全局变量或函数名占用全局变量名的情况
- 与此同时它能在禁止访问函数内声明变量的情况下允许外部对函数的调用
作用
1. 隐藏函数的具体实现
2. 不会污染外部(全局)命名空间
3. 用它来编码js模块
示例
<script type="text/javascript">
//匿名函数自调用
(function () {
var a = 3
console.log(a + 3)
})()
var a = 4
console.log(a)
//立即执行函数代码前不加";" 浏览器会报错
;(function () {
var a = 1
function test () {
console.log(++a)
}
window.$ = function () { // 向外暴露一个全局函数
return {
test: test
}
}
})()
$().test() // 1. $是一个函数 2. $执行后返回的是一个对象
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 7. this
解析器在调用函数每次都会向函数内部传递进一个隐含的参数,这个隐含的参数就是this,this指向的是调用函数的当前对象,这个对象我们称为函数执行的上下文对象。
根据函数的调用方式的不同,this会指向不同的对象
- 以函数的形式调用时,this永远都是指向全局对象window
- 以方法的形式调用时,this就是指向调用方法的那个对象
- 以构造函数调用时,this就是指向新创建的对象
- 以call或apply方法调用时,this指向的是我们指定传入方法中的那个实参
示例
<script type="text/javascript">
function Person(color) {
console.log(this)
this.color = color;
this.getColor = function () {
console.log(this)
return this.color;
};
this.setColor = function (color) {
console.log(this)
this.color = color;
};
}
Person("red"); //this是谁? window
var p = new Person("yello"); //this是谁? p
p.getColor(); //this是谁? p
var obj = {};
p.setColor.call(obj, "black"); //this是谁? obj
var test = p.setColor;
test(); //this是谁? window
function fun1() {
function fun2() {
console.log(this);
}
fun2(); //this是谁? window
}
fun1();
</script>
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
# 函数高级
# 8. 原型(prototype)
1.函数的prototype属性
- 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
- 原型对象中有一个属性constructor, 它指向函数对象
2.给原型对象添加属性(一般都是方法)
- 作用: 函数的所有实例对象自动拥有原型中的属性(方法)
<script type="text/javascript">
// 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
console.log(Date.prototype, typeof Date.prototype)
function Fun () {
}
console.log(Fun.prototype) // 默认指向一个Object空对象(没有我们的属性)
// 原型对象中有一个属性constructor, 它指向函数对象
console.log(Date.prototype.constructor===Date)
console.log(Fun.prototype.constructor===Fun)
//给原型对象添加属性(一般是方法) ===>实例对象可以访问
Fun.prototype.test = function () {
console.log('test()')
}
var fun = new Fun()
fun.test()
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 9. 显式原型与隐式原型
- 每个函数function都有一个prototype,即显式原型(属性)
- 每个实例对象都有一个_proto_,可称为隐式原型(属性)
- 对象的隐式原型的值为其对应构造函数的显式原型的值,即实例的隐式原型属性永远指向自身构造函数的显式原型属性
总结:
1. 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象
2. 对象的\__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
3. 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
示例
<script type="text/javascript">
//定义构造函数
function Fn() {
// 内部语句: this.prototype = {}
}
// 1. 每个函数function都有一个prototype,即显式原型属性, 默认指向一个空的Object对象
console.log(Fn.prototype)
// 2. 每个实例对象都有一个__proto__,可称为隐式原型
//创建实例对象
var fn = new Fn() // 内部语句: this.__proto__ = Fn.prototype
console.log(fn.__proto__)
// 3. 对象的隐式原型的值为其对应构造函数的显式原型的值
console.log(Fn.prototype===fn.__proto__) // true
//给原型添加方法
Fn.prototype.test = function () {
console.log('test()')
}
//通过实例调用原型的方法
fn.test()
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 10. 原型链
别名: 隐式原型链
作用: 查找对象的属性(方法)
总结步骤
- Object的显式原型的隐式原型为null, 即原型链的尽头
- 访问一个对象的属性时,先在自身属性中查找,找到返回
- 如果没有, 再沿着__proto__这条链向上查找,找到返回,如果到了原型链尽头还是没找到, 返回undefined
# 10.1 构造函数/原型/实例对象的关系
- 对于每一个函数, 包括Object构造函数, 都有一个隐式原型指向Function构造函数的显式原型, 即每一个函数都是Function构造函数的实例
- Function也是一个构造函数, 因此它的隐式原型指向它自己的显式原型, 即Function也是自己的实例
- prototype也是一个对象, 因此它的隐式原型指向Object构造函数的显式原型, 即所有的prototype是Object构造函数的实例
- Object的显式原型也有隐式原型, 由于它已经是原型链的尽头所以值为null
# 10.2 属性问题
- 读取对象的属性值时:会自动到原型链中查找
- 设置对象的属性值时:不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
- 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
示例代码
<script type="text/javascript">
function Fn() {
}
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a, fn1)
var fn2 = new Fn()
fn2.a = 'yyy'
console.log(fn1.a, fn2.a, fn2)
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 12)
p1.setName('Bob')
console.log(p1)
var p2 = new Person('Jack', 12)
p2.setName('Cat')
console.log(p2)
console.log(p1.__proto__===p2.__proto__) // true
</script>
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
# 10.3 探索instanceof
- instance是如何判断的
- 表达式:
A instanceof B
- 如果B函数的显式原型在A函数的原型链上则返回true,否则返回false
- 表达式:
- Function是通过new自己产生的实例
例子
Function instanceof Object //true Function.__proto__.__proto__ === Object.prototype
Object instanceof Object //true Object.__proto__.__proto__ === Object.prototype
Object instanceof Functon //true Object.__proto__ === Function.prototype
Function instanceof Function //true Function.__proto__ === Function.prototype
function foo(){}
Object instanceof foo //false
2
3
4
5
6
# 10.4 属性问题
- 读取对象的属性值时:会自动到原型链中查找
- 设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性, 直接添加此属性并设置其值
- 方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
示例代码
<script type="text/javascript">
function Fn() {
}
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a, fn1)
var fn2 = new Fn()
fn2.a = 'yyy'
console.log(fn1.a, fn2.a, fn2)
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 12)
p1.setName('Bob')
console.log(p1)
var p2 = new Person('Jack', 12)
p2.setName('Cat')
console.log(p2)
console.log(p1.__proto__===p2.__proto__) // true
</script>
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
# 10.5 相关面试题
面试题一
var A = function(){};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n:2,
m:3
}
var c = new A();
console.log(b.n,b.m,c.n,c.m); //1 undefined 2 3
2
3
4
5
6
7
8
9
面试题二
function F(){}
Object.prototype.a = function(){
console.log("a");
}
Function.prototype.b = function(){
console.log("b");
}
var f = new F();
f.a(); //a
f.b(); //f.b() is not a function
F.a(); //a
F.b(); //b
2
3
4
5
6
7
8
9
10
11
12
# 11. 执行上下文与执行上下文栈
# 11.1 变量提升与函数提升
变量声明提升
- 通过var关键字定义的变量,在这行定义语句前就能访问到
- 值: undefined
函数声明提升
- 通过function关键字声明的函数,在声明之前就能访问到
- 值: 通过function关键字定义的函数本身
注1:顺序上先变量提升再函数提升,优先级上函数提升高于变量提升,即如果某变量和某函数同名,则它们都发生了提升,顺序上后提升的函数会覆盖掉同名的变量
注2:在函数中使用未声明的变量会自动声明成全局变量
注3:通过表达式定义并被变量接收的函数不能函数提升
示例代码
<script type="text/javascript">
console.log('-----')
/*
面试题 : 输出 undefined
*/
var a = 3
function fn () {
console.log(a)
var a = 4
}
fn()
console.log(b) //undefined 变量提升
fn2() //可调用 函数提升
// fn3() //不能 变量提升
var b = 3
function fn2() {
console.log('fn2()')
}
var fn3 = function () {
console.log('fn3()')
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 11.2 执行上下文
1.代码分类
- 全局代码
- 函数代码
2.全局执行上下文
1.在执行全局代码前将window确定为全局执行上下文
2.对全局数据进行预处理
①var定义的全局变量--->赋值为undefined, 添加为window的属性
②function关键字声明的全局函数--->赋值为这个函数, 将全局函数添加为window的方法
③this--->赋值为window
3.开始执行全局代码
3.函数执行上下文
1.在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象
2.对局部数据进行预处理
①声明形参变量--->赋值为实参--->添加为函数执行上下文的属性
②arguments--->赋值为实参列表, 添加为函数执行上下文属性
③var定义的局部变量--->赋值为undefined, 添加为函数执行上下文属性
④function声明的函数--->赋值为这个函数本身, 添加为函数执行上下文方法
⑤this--->赋值为调用这个函数的对象
3.开始执行函数体代码
注1:执行上下文就是一个函数或者全局代码运行时的环境,里面包括执行时需要用到的数据,可以理解为是一个对象,但它并不是真正的对象,它只是栈内存中被开辟出来的一块区域
注2:函数每次执行都会创建一个新的执行上下文(不管是否重复)
注3:函数每次执行完毕都会销毁这个执行上下文
# 11.3 执行上下文栈(call stack)
- 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文
- 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
- 当一个函数运行时,会在运行前生成一个执行上下文并入栈,函数运行结束就会出栈
- 每次正在运行的函数使用的都是栈顶的上下文
- 栈底永远是全局上下文,当所有的代码执行完后, 栈中只剩下window
流程分析图
注意上图的情况说明fn函数一定是在bar函数体代码中被调用了
# 11.4 相关面试题
<script type="text/javascript">
/*
测试题1: 先执行变量提升, 再执行函数提升
*/
function a() {}
var a
console.log(typeof a) // 'function'
/*
测试题2
ES5中没有块作用域的概念,所以var声明的变量仍然有变量提升
相当于先在全局中声明变量b,值为undefined,但赋值为1仍需在if块中执行
但此时b已经是window的属性了,所以进不到if的语句执行块中,最后只能直接打印
*/
if (!(b in window)) {
var b = 1
}
console.log(b) // undefined
/*
测试题3
*/
var c = 1
function c(c) {
console.log(c)
var c = 3
}
c(2) // 报错
</script>
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
重点说下测试题3,报错的原因是存在变量提升与函数提升,这两种提升都是在预处理阶段执行的,但是预处理阶段无论是全局还是函数执行上下文,var定义的变量都要被赋值为undefined,然后就结束了,开始执行全局/函数体代码,这阶段一开始才会对var定义的变量进行真正值的赋值操作(仍在其他非声明代码前),即执行的代码等同于下面的效果:
var c;
function c(c) {
console.log(c)
var c = 3
}
c=1;
c(2);
2
3
4
5
6
7
所以虽然函数名也是c,但c最后又被覆盖成了一个字面量1,自然就会报错c不是一个函数了。
# 12. 作用域与作用域链
# 12.1 作用域
理解
1. 作用域就是一块"地盘", 一个代码段所在的区域
2. **它是静态的(相对于上下文对象), 在编写代码时就确定了**
分类
1. 全局作用域
2. 函数作用域
3. 没有块作用域(ES6有了)
起到的作用
- 隔离变量,不同作用域下同名变量不会有冲突
# 12.2 作用域与执行上下文
区别1
- 全局作用域之外,每个函数都会创建自己的作用域
- 函数作用域在函数定义时而非函数调用时创建
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文在调用函数时, 函数体代码执行前创建
区别2
- 作用域是静态的, 只要函数定义好后就一直存在且不会再变化
- 执行上下文是动态的, 调用函数时创建, 函数执行完成后自动销毁
联系
- 作用域从属于所在的执行上下文
- 全局作用域==>全局执行上下文
- 函数作用域==>函数执行上下文
# 12.3 作用域链
理解
- 多个上下级关系的作用域形成的链的方向是从内向外的
- 查找变量时是沿着作用域链来查找的
查找一个变量的规则
- 在当前作用域下的执行上下文中查找对应属性, 有则返回,没有则进入第二步
- 在上一级作用域下的执行上下文中查找对应属性, 有则返回,没有则进入第三步
- 依次执行第二步,直到处在全局作用域中, 在全局上下文中查找对应属性, 有则返回,没有就抛出找不到的异常
# 13. 闭包
# 13.1 理解闭包
一、如何产生闭包?
1. 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量/函数时, 就产生了闭包
2. **执行嵌套的外部函数定义**就会产生闭包(不用调用嵌套的内部函数)
注:2021年后需要调用内部函数才能产生闭包
二、闭包到底是什么?
- 理解一: 闭包是嵌套的内部函数(常见理解)
- 理解二: 包含被引用变量(函数)的对象
注1: 闭包存在于嵌套的内部函数中
注2:闭包可以理解为能够读取其他函数内部变量的函数,由于在JavaScript中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
三、产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
参照链接
https://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
# 13.2 常见闭包
- 将函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数调用
<script type="text/javascript">
// 1. 将函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2//注意这里返回的是嵌套的内部函数对象
}
var f = fn1()//所以f接收到的是函数对象f2,加个()才能运行
f() // 3
f() // 4
// 2. 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function () {
alert(msg)//只有msg是被引用的外部变量,闭包就是这个传进setTimeout的函数
}, time)//参照13.1节 第二点的注2,setTimeout不是定义在showDelay函数内部的函数,所以此处无闭包
//可以参照阮一峰的文章进行理解
}
showDelay('atguigu', 2000)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 13.3 闭包的作用
- 外部函数在执行完毕后,闭包能使外部函数的内部变量仍然存留在内存中, 即延长了变量的生命周期
- 未成为闭包中的变量会在外部函数执行完毕后被释放
- 让函数外部可以操作到函数内部的数据但并非将变量直接暴露给外部
# 13.4 闭包的生命周期
- 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
- 死亡: 在嵌套的内部函数成为垃圾对象时
示例代码
<script type="text/javascript">
function fn1() {
//此时闭包就已经产生了(函数提升, 内部函数对象已经创建了)
var a = 2
function fn2 () {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 13.5 闭包的作用(定义JS模块)
- JS模块是具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(只有在函数作用域中数据才是私有的)
- 只向外暴露一个包含n个方法的对象或函数(只有一个方法暴露就函数做返回值,暴露多个方法就返回对象,所有要暴露的函数做该对象的属性)
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
示例代码
方式一
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule()
module.doSomething()
module.doOtherthing()
</script>
2
3
4
5
6
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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
方式二
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
myModule2.doSomething()
myModule2.doOtherthing()
</script>
2
3
4
5
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
}
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
或以下代码,声明了window做参数,好处就是代码压缩
(function (window) {
//私有数据
var msg = 'My atguigu'
//操作数据的函数
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}
//向外暴露对象(给外部使用的方法)
window.myModule2 = {
doSomething: doSomething,
doOtherthing: doOtherthing
}
})(window)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 13.6 闭包的缺点及解决
缺点
1. 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
2. 容易造成内存泄露
解决方法
1. 能不用闭包就不用
2. 及时释放(内部函数成为垃圾对象-->回收闭包)
**内存溢出:**程序需要的内存超出内存剩余的内存,此时程序运行时出现错误
**内存泄露:**占用的内存没有及时释放,内存泄露过多容易导致内存溢出
常见的内存泄露
- 意外的全局变量
- 没有及时处理的计时器或回调函数
- 闭包
# 13.7 相关面试题
代码片段一中object.getNameFunc()返回的是函数对象function(){return this.name;}
,此时再加一个(),则执行该函数,但此时该函数不是通过普通对象来调的,所以它的调用者是全局对象window,那么该函数中的this自然指向window,所以返回值是The Window
代码片段二的不同点在于用that变量保存了object2对象的地址值,所以调用者仍是全局对象window,但返回的值是object对象里的name属性值
只有代码片段二有闭包,因为用到了外层函数的变量that
<script type="text/javascript">
//代码片段一
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //? the window
//代码片段二
var name2 = "The Window";
var object2 = {
name2 : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name2;
};
}
};
alert(object2.getNameFunc()()); //? my object
</script>
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
fun(0)没传实参给形参o,则实际等同于fun(0,undefined),所以a赋值为fun(0)时就已经打印了undefined,此时a是一个内有fun属性(或者说函数)的对象,注意这个fun函数在一开始定义的时候就成为了一个闭包,引用的外部变量是n。而n此时的值正是0,接下来a.fun(1),即调用了对象a里面的函数fun,形参m的值为1,所以最后返回的是外部函数fun的调用,也就是fun(1,0),因为闭包的缘故,n变量一直存在且值为0,那么最后的打印结果就是0(打印语句在改变n值前)
类似地,a.fun(2)和a.fun(3)都是分开调用,所以n的值一直是0,所以分别输出0,0
再来说b,fun(0).fun(1)这部分和前面相差不大,所以分别输出undefined,0,然后fun(0).fun(1).fun(2),n在fun(0).fun(1)调用后,值已经改为了1,所以先打印输出1,然后再将n的值变为2,最后fun(0).fun(1).fun(2).fun(3)也类似,先打印输出2,然后再将n的值变为3。这里的关键点在于函数表达式是连续调用的,且闭包存在,所以n的值不断被改变,并保存在内存中
最后到c,类似于前文分析,c先输出undefined,0,又因为c.fun(2)和c.fun(3)分开调用,打印语句在改变n值前,所以输出的结果一直都是fun(0).fun(1)执行后赋给n的值,也就是1
<script type="text/javascript">
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n)
}
}
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)//undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3)//undefined,0,1,2
var c = fun(0).fun(1)
c.fun(2)
c.fun(3)//undefined,0,1,1
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 对象高级
# 14. 对象创建模式
# 14.1 Object构造函数模式
- 套路: 先用new关键字创建空对象, 再动态添加新的属性/方法
- 使用场景: 起始时不知道对象内部的数据
- 问题: 语句太多
var obj = new Object();
obj.name = "a";
obj.age = "1";
obj.setname = function(){
this.name = "b";
};
2
3
4
5
6
# 14.2 对象字面量模式
- 套路: 使用{}创建空对象, 再动态添加新的属性/方法
- 使用场景: 起始时已知对象内部的数据
- 问题: 如果创建多个对象, 代码会重复
var obj = {
name: "a",
age: 1,
setname: function(){
this.name = "b";
}
};
2
3
4
5
6
7
# 14.3 工厂模式
- 套路: 使用工厂函数动态创建对象并返回
- 使用场景: 需要创建多个对象
- 问题: 对象没有具体类型, 都是Object
function createPerson(name, age){
var obj = new Object();
obj.name = "a";
obj.age = "1";
obj.setname = function(){
this.name = "b";
};
return obj;
}
2
3
4
5
6
7
8
9
# 14.4 自定义构造函数
- 套路: 自定义构造函数, 使用new创建
- 使用场景: 需要创建多个类型确定的对象
- 问题: 如果实例化多次会使同样的方法重复占用内存空间
function createPerson(name, age){
this.name = "a";
this.age = "1";
this.setname = function(){
this.name = "b";
};
}
2
3
4
5
6
7
# 14.5 自定义构造函数+原型对象组合使用
- 套路: 自定义构造函数, 使用new创建, 添加方法时使用原型对象来添加
- 使用场景: 需要创建多个类型确定的对象
function createPerson(name, age){
this.name = "a";
this.age = "1";
}
createPerson.prototype = {
getname: function(){
this.name = "b";
}
}
2
3
4
5
6
7
8
9
# 15. 继承模式
# 15.1 原型链继承
操作步骤
- 定义父类型构造函数
- 给父类型的原型添加方法
- 定义子类型的构造函数
- 创建父类型的实例对象并赋值给子类型的原型(关键一步,此处时原型链能够继承)
- 将子类型原型的构造属性设置为子类型
- 给子类型原型添加方法
- 创建子类型的对象: 可以调用父类型的方法
缺点是如果父函数有一个变量为引用类型, 任意一个实例修改这个变量会导致所有实例的相关属性被修改
示例代码
<script type="text/javascript">
//父类型
function Supper() {
this.supProp = 'Supper property'
}
Supper.prototype.showSupperProp = function () {
console.log(this.supProp)
}
//子类型
function Sub() {
this.subProp = 'Sub property'
}
// 子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper()
// 让子类型的原型的constructor指向子类型
Sub.prototype.constructor = Sub
Sub.prototype.showSubProp = function () {
console.log(this.subProp)
}
var sub = new Sub()
sub.showSupperProp()
// sub.toString()
sub.showSubProp()
console.log(sub) // Sub
</script>
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
# 15.2 借用构造函数继承
- 假继承, 没有继承父类型方法
- 缺点是父类有方法时会被创建多次
操作步骤
- 创建父类型构造函数
- 创建子类型构造函数
- 在子类型构造函数中使用call/apply调用父类型构造函数
示例代码
<script type="text/javascript">
function Person(name, age) {
this.name = name
this.age = age
}
function Student(name, age, price) {
Person.call(this, name, age) // 相当于: this.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price
}
var s = new Student('Tom', 20, 14000)
console.log(s.name, s.age, s.price)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 15.3 寄生式继承
缺点是方法没有放到原型中,无法复用
操作步骤
- 创建父类型构造函数
- 创建子类型构造函数
- 在子类型构造函数中使用call/apply调用父类型构造函数
示例代码
var Person = function(o){
var obj = Object.create(o); //返回一个隐式原型为o的实例
obj.class = "student";
obj.say = function(){
console.log(this.name);
}
return obj;
};
var aman = {
name: "tom",
age: 16
}
var student = new Person(aman);
2
3
4
5
6
7
8
9
10
11
12
13
# 15.4 组合继承
- 借用构造函数继承+原型继承
- 缺点是构造函数执行了两次
操作步骤
- 创建父类型构造函数
- 创建子类型构造函数
- 在子类型构造函数中使用call/apply调用父类型构造函数
示例代码
var Person = function(name, age){
this.name = name;
this.age = age;
};
Person.prototype.setName = function(name){
this.name = name;
};
Person.prototype.setAge = function(age){
this.age = age;
};
var Student = function(name, age, price){
Person.call(this, name, age);
this.price = price;
}
Student.prototype = new Person("tom", 12);
Student.prototype.constructor = Student;
Student.prototype.setPrice = function(price){
this.price = price;
};
var student = new Student('aa',1,2)
console.log(student)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 线程机制与事件机制
# 16. 进程与线程
概念
- 进程:程序的一次执行,它占有一片独有的内存空间
- 线程: 进程里的一个独立执行单元,CPU的基本调度单位,是程序执行的一个完整流程
要点
- 一个进程中一般至少有一个运行的线程: 主线程,进程启动后自动创建
- 一个进程中也可以同时运行多个线程
- 一个进程内的数据可以供其中的多个线程直接共享
- 多个进程之间的数据是不能直接共享的,即数据相互独立
- 应用程序必须运行在某个进程的某个线程上
- 线程池: 保存多个线程对象的容器, 实现线程的重复利用
# 16.1 比较单线程与多线程
**多线程优点:**CPU利用效率高
多线程缺点
- 创建多线程开销
- 线程间切换开销
- 死锁与状态同步问题
**单线程优点:**顺序编程简单易懂 **单线程缺点:**效率低
# 16.2 浏览器与进程/线程
Q:浏览器运行是单线程还是多线程? A:多线程
Q:浏览器运行是单进程还是多进程? A:有单进程如火狐与老版IE 也有多进程如Chrome与新版IE
Q:如何查看浏览器是否是多进程运行的呢?
A:任务管理器==>进程
# 17. 浏览器内核
- 内核是支撑浏览器运行的最核心的程序
- 不同浏览器的内核可能不一样
- 内核由多个模块组成
主线程模块
- js引擎模块, 负责js的编译与运行
- html, css文档解析模块, 负责页面文本的解析
- DOM/CSS模块, 负责DOM/CSS在内存中的相关处理
- 布局和渲染模块, 负责页面的布局与效果的绘制(内存中的对象)
- ...
分线程模块
- 定时器模块, 负责定时器管理
- 事件响应模块, 负责事件的管理
- 网络请求模块, 负责ajax请求
# 18. 启动定时器
定时器真的是定时执行吗?
- 无法保证真正定时执行
- 一般会延迟一点, 也可能延迟很长时间
- 延迟很久的情况,一般是因为定时器之外的其他代码执行花费了不少时间
定时器的回调函数是在分线程执行的吗?
答:不是, js是单线程的
定时器如何实现的?
答:依靠事件循环模型实现
# 19. JS是单线程执行的
如何证明js执行是单线程的?
- setTimeout()函数是在主线程执行的
- 定时器回调代码只有在运行栈中的代码全部执行完后才执行
为什么js要用单线程模式而不是多线程模式?
答:作为浏览器脚本语言主要用途在于与用户互动及操作DOM, 这决定必须为单线程执行, 否则有严重的同步问题
# 19.1 代码分类
- 初始化执行代码(同步代码): 包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码
- 回调执行代码(异步代码): 处理回调逻辑
# 19.2 js引擎执行代码的基本流程
1.先执行初始化代码, 包含一些特殊代码
- 设置定时器
- 绑定监听
- 发送ajax请求
2.某个时刻后再执行回调代码 (使用alert()能暂停主线程执行与定时器计时)
# 20. 事件循环模型
模型的2个重要组成部分
1. 事件(定时器/DOM事件/Ajax)管理模块
2. 回调队列
模型的运转流程
1. 执行初始化代码, 将事件回调函数交给对应模块管理
2. 当事件发生时, 管理模块会将回调函数及其数据添加到回调列队中
3. 只有当初始化代码执行完后(可能要一定时间), 才会遍历读取回调队列中的回调函数执行

# 21. H5 Web Workers多线程
介绍
- Web Workers是HTML5的多线程解决方案,JS分线程通过它实现
- 可以将一些大计算量的代码交由Web Workers运行而不冻结用户界面
- 但是子线程完全受主线程控制且不能操作DOM, 因此没有改变JS是单线程的属性
不足
- 慢
- 不能跨域加载js
- worker内代码不能操作dom
- 不是每个浏览器都支持
示例代码
<body>
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value
//创建一个Worker对象
var worker = new Worker('worker.js')
// 绑定接收消息的监听
worker.onmessage = function (event) {
console.log('主线程接收分线程返回的数据: '+event.data)
alert(event.data)
}
// 向分线程发送消息
worker.postMessage(number)
console.log('主线程向分线程发送数据: '+number)
}
// console.log(this) // window
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
worker.js
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用
}
console.log(this)
this.onmessage = function (event) {
var number = event.data
console.log('分线程接收到主线程发送的数据: '+number)
//计算
var result = fibonacci(number)
postMessage(result)
console.log('分线程向主线程返回数据: '+result)
// alert(result) alert是window的方法, 在分线程不能调用
// 分线程中的全局对象不再是window, 所以在分线程中不可能更新界面
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15