一、原型与原先链

原型(prototype)

1、函数的prototype属性

每个函数都有一个prototype属性。它默认指向一个Object空对象(即成为:原型对象)

原型对象中有一个属性constructor,它指向函数对象

2、给原型对象添加属性(一般都是方法)

作用:函数的所有实例对象自动拥有原型中的属性(方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 每个函数都有一个prototype属性。它默认指向一个Object空对象(即成为:原型对象)
console.log(Date.prototype, typeof Date.prototype);
function fun(){

}

console.log(fun.prototype);// 默认指向一个object空对象(没有我们定义的属性)

// 原型对象中有一个属性constructor,它指向函数对象
console.log(Date.prototype.constructor === Date); // true
console.log(fun.prototype.constructor === fun); // true

// 给原型对象添加属性(一般是方法)——> 实例对象可以访问
fun.prototype.test = function() {
console.log('test原型对象添加');
}
var f = new fun();
f.test(); // test原型对象添加

原型

显式原型与隐式原型

每个函数function都有一个prototype,即显式原型属性,默认指向一个空的object对象

每个实例对象都有一个_proto_ ,可称为隐式原型

对象的隐式原型的值为其对应构造函数的显式原型的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Fn(){ // 内部语句:this.prototype = {}

}
// 每个函数function都有一个prototype,即显式原型属性,默认指向一个空的object对象
console.log(Fn.prototype);
// 每个实例对象都有一个__proto__。可以称为隐式原型
var fn = new Fn(); // 内部语句:this.__proto__ = Fn.prototype
console.log(fn.__proto__);
// 对象的隐式原型的值为其对应构造函数的显示原型的值
console.log(Fn.prototype === fn.__proto__);// true
// 给原型对象添加方法
Fn.prototype.test = function(){
console.log('给原型对象添加方法')
}
fn.test()

显式原型与隐式原型

函数的prototype属性:在定义函数时自动添加的,默认值是一个空Object对象

对象__proto__属性:创建对象时自动添加的,默认值为构造函数的prototype属性值

程序员能直接操作显式原型,但不能直接操作隐式原型(ES6之前)

原型链

1
2
3
4
5
6
7
8
9
10
11
12
function Fn(){
this.test1 = function(){
console.log('test()');
}
}
Fn.prototype.test2 = function(){
console.log('test2()')
}

var fn = new Fn();
fn.test1();
fn.test2();

原型链

访问一个对象属性时,

​ 先在自身属性中查找,找到则返回

​ 如果没有,再沿着__proto__这条线往上找,找到则返回

​ 如果最终没找到,返回undefined

原型链别名:隐式原型链

作用:查找对象属性

构造函数/原型/实例对象的关系

1、函数的显示原型指向的对象:默认是空Object实例对象(但Object不满足)

2、所有函数都是Function的实例(包含Function自身)

3、Object的原型对象是原型链的尽头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Fn(){
this.test1 = function() {
console.log('test1()');
}
}

var fn = new Fn();

console.log(Fn.prototype instanceof Object); // true
console.log(Object.prototype instanceof Object); // false
console.log(Function.prototype instanceof Object); // true

// 所有函数都是Function的实例(包含Function自身)
console.log(Function.__proto__ === Function.prototype); // true

// Object的原型对象是原型链的尽头
console.log(Object.prototype.__proto__); // null

原型链属性

1、读取对象属性值时:会自动到原型链中查找

2、设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加属性并设置值

原型属性

3、方法一般定义在原型中,属性一般通过构造函数定义在对象本身上

原型属性

instanceof

instanceof是如何判断的

表达式:A instanceof B

如果 B函数对象的显示原型对象在 A对象的原型链上,返回true,否则返回false

原型链查找1

Function是通过new自己产生的实例

原型链查找2

二、执行上下文与执行上下文栈

变量提升与函数提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 输出undefined
var a = 3;
function fn() {
console.log(a);
var a = 4;
}
fn();
// 在fn函数内部 var a = 4; 实际上等于下列代码
function fn() {
var a; // 这里的 var a; 就是变量提升
console.log(a); // 所以这里输出undefined
a = 4;
}

// fn2 在通过 function 声明函数, 在之前就可以直接调用。函数声明提升
fn2();
function fn2() {
console.log('fn2()');
}

fn3(); // 这里不能调用,因为下面属于是变量提升,并不是函数提升。 函数提升需要通过 function 声明
var fn3 = function() {
console.log('fn3()');
}

1、变量声明提升

  • 通过 var 定义(声明)的变量,在定义语句之前就可以访问到
  • 值:undefined

2、函数声明提升

  • 通过 function 声明的函数,在之前就可以直接调用
  • 值:函数定义(对象)

执行上下文

代码分类(位置)

全局代码

函数(局部)代码

全局执行上下文

在执行全局代码前将window确定为全局执行上下文

对全局数据进行预处理

​ var定义的全局变量 ===》undefined,添加为window的属性

​ function声明的全局函数 ===》赋值(fun),添加为window的方法

开始执行全局代码

函数执行上下文

在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)

对局部数据进行预处理

​ 形参变量 ===》赋值(实参) ===》 添加为执行上下文的属性

​ arguments ==》赋值(实参列表),添加为执行上下文属性

​ var定义的局部变量 ===》undefined,添加为执行上下文的属性

​ function声明的函数 ===》 赋值(fun),添加为执行上下文的方法

​ this ===》赋值(调用函数的对象)

开始执行函数体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数执行上下文
function fn(a1) {
console.log(a1); // 2
console.log(a2); // undefined
a3(); // a3()
console.log(this); // window,这里是window因为下面的调用方式是fn(2, 3)
console.log(arguments); // 伪数组(2,3)

var a2 = 3;
function a3() {
console.log('a3()');
}
}
fn(2, 3);

执行上下文栈

在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象

在全局执行上下文(window)确定后,将其添加到栈中(压栈)

在函数执行上下文创建后,将其添加到栈中(压栈)

在当前函数执行后,将栈顶的对象移除(出栈)

当所有代码执行完成后,栈中只剩下window

1
2
3
4
5
6
7
8
9
10
var a = 10;
var bar = function(x) {
var b = ;
foo(x + b);
}
var foo = function(y) {
var c = 5;
console.log(a + c + y); // 下图不考虑改打印函数的上下文
}
bar(10)

执行函数上下文栈

例题

1、

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
/**
* 1、依次输出了什么?
* 2、整个过程中产生了几个执行上下文?
*/
console.log('global begin:' + i);
var i = 1;
foo(1);
function foo(i) {
if (i == 4) {
return;
}
console.log('foo() begin:' + i);
foo(i + 1); // 在函数中调用自己称为递归
console.log('foo() end:' + i);
}
console.log('global end:' + i);

/**
1:
undefined
foo() begin:1
foo() begin:2
foo() begin:3
foo() end:3
foo() end:2
foo() end:1
global end:1

每次调用自己时并没有执行到 foo() end,而在 i == 4 中return后函数会按照栈中顺序一个一个执行剩余代码并出栈,所以此时执行foo() end
*/

/**
2:不考虑console.log()
4 + 1 次(4次foo,一次全局)
*/

执行上下文栈例题1

2、

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
/**
* 1
*/
function a() {}
var a;
console.log(typeof a); // 输出:function。因为先进行变量提升再进行函数提升。所以后提升的函数a()生效

/**
* 2
*/
if (!(b in window)) { // b in window 为true 因为写了 var b,即已进行变量提升
var b = 1;
}
console.log(b); // undefined 下列为测试代码
if (false) {
var b = 1;
}
console.log(b in window); // true

/**
* 3
*/
var c = 1;
function c(c) {
console.log(c);
}
c(2); // 报错,c is not a function 原因:先进行变量提升,然后进行函数提升,最后执行代码。
// 相当于下列代码,先声明c,在执行c(2)前对c进行赋值,使其变成变量,并非一个function
var c;// 先变量提升
function c(c) { // 后函数提升,函数提升时执行(执行不代表调用)
console.log(c);
}
c = 1; // 变量提升与函数提升后执行进行赋值
c(2);

三、作用域与作用域链

作用域

理解

作用域就是一块“地盘”,一个代码短所在区域

它是静态的(相对于上下文对象),在编写代码时就确定了

分类

全局作用域

函数作用域

没有块作用域(ES6有)

作用

隔离变量,不同作用域下同名变量不会冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (true) {
var a = 4; // 在大括号外部也可以访问到a,没有大括号的块作用域
}

// 全局作用域
var a = 10,
b = 20;
function fn(x) { // 函数作用域 fn
var a =100, // 变量隔离
c = 300;
console.log('fn()', a, b, c, x);
function bar(x) { // 函数作用域 bar
var a = 1000, // 变量隔离
d = 400;
console.log('bar()', a, b, c, d, x);
}

bar(100);
bar(200);
}
fn(10);

作用域与全局上下文

区别

1、

全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时

全局执行上下文环境时在全局作用域确定之后,js代码马上执行之前创建

函数执行上下文环境时在调用函数时,函数体代码执行之前创建

2、

作用域时静态的,只要函数定义好了就一直存在,且不会再变化

执行上下文环境时动态的,调用函数时创建,函数调用结束时上下文环境就会自动释放

3、

执行上下文环境(对象)时从属于所在的作用域

全局上下文环境==>全局作用域

函数上下文环境==>对应函数作用域

作用域链

理解

多个上下级关系的作用域形成的链,他的方向是从下向上的(从内到外)

查找变量时就是沿着作用域链来查找的

查找一个变量的查找规则

在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入2

在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入3

再次执行2的相同操作,直到全局作用域,如果还找不到就抛出找不到的异常(xxx is not defined)

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;
function fn1() {
var b = 2;
function fn2() {
var c = 3;
console.log(c); // 3
console.log(b); // 2
console.log(a); // 1
console.log(d); // 报错 d is not defined
}
fn2();
}
fn1();

四、闭包

例子:实现按钮循环监听事件,点击相应按钮弹出点击的是第几个

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
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<script type="text/javascript">
var btns = document.getElementsByTagName('button');
// 遍历加监听
/*
这种方式无法正确弹出按钮所对应的值,因为 i 是在全局作用域中的变量,当for循环绑定事件后,点击执行事件函数函数中 i 访问到的是外部的全局变量,而此时 i 的值是循环后的值
for (var i = 0,lengths = btns.length; i < lengths; i++) {
var btn = btns[i];
btn.onclick = function() {
alert('第' + (i + 1) + '个');
}
}*/
/*
将下标保存到按钮上
for (var i = 0,lengths = btns.length; i < lengths; i++) {
var btn = btns[i];
btn.index = i; // 将btn所对应的下标保存到btn上
btn.onclick = function() {
alert('第' + (this.index + 1) + '个');
}
}*/

// 利用闭包
for (var i = 0,lengths = btns.length; i < lengths; i++) {
(function(i) {
var btn = btns[i];
btn.onclick = function() {
alert('第' + (this.index + 1) + '个');
}
})(i);
}
</script>
</body>

理解

如何产生闭包?

当一个嵌套的内部(子)函数引用了嵌套外部(父)函数的变量(函数)时,就产生了闭包

闭包到底是什么?

闭包是嵌套的内部函数包含被引用外部变量(函数)的对象

产生闭包的条件?

函数嵌套,内部函数引用了外部函数的数据(变量/函数)

如上图所示,可通过chrome调试模式查看Closure(闭包),当执内部行函数定义(并不是调用函数)时就产生了闭包。执行内部函数定义需要先调用外部函数,并不需要调用内部函数。

常见的闭包

1、将函数作为另一个函数的返回值

1
2
3
4
5
6
7
8
9
10
11
function fn1() {
var a= 2;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1(); // 这里进行调用外部函数产生了 1 个闭包
f(); // 输出 3
f(); // 输出 4

2、将函数作为实参传递给另一个函数调用

1
2
3
4
5
6
function fn(msg, time) {
setTimeout(function() {
alert(msg);
}, time);
}
fn('msg', 2000);

闭包的作用

使用函数内部的变量在函数执行后,仍然存活在内存中(延长了局部变量的声明周期)

让函数外部可以操作(读写)到函数内部的数据(变量/函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 下列代码中就起到了对变量a的生命周期延长,当fn1执行后fn2与fn3产生了闭包,通过返回fn2或者fn3并接收调用则可实现对变量a的加1或减1操作,而除了加减外不可对变量a进行其它操作。
function fn1() {
var a = 2;
function fn2() {
a++;
console.log(a);
}
function fn3() {
a--;
console.log(a);
}
return fn3;
}
var f = fn1();
f();

问题

1、函数执行之后,函数内部声明的局部变量是否还存在?

​ 一般时不存在。存在于闭包中的变量才可能会存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fn1() {
var a = 2;
function fn2() {
a++;
console.log(a);
}
function fn3() {
a--;
console.log(a);
}
return fn3;
}
var f = fn1(); // fn1的函数执行完成后会释放,这里返回了fn3被变量f所引用。所以内部函数fn3并没有被释放。
// fn1(); 这里如果没有引用指向则执行完成后就会成为垃圾对象被释放。
f();

2、在函数外部能直接访问函数内部的局部变量吗?

​ 不能,但是我们可以通过闭包让外部间接操作它。例如上面代码所示。

闭包的生命周期

1、产生:在嵌套内部函数定义执行完时就产生了(不是调用)

2、死亡:在嵌套的内部函数成为垃圾对象时

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn1() {
// 此时闭包就已经产生了(函数提升,内部函数对象已经创建了)
var a = 2;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f();
f();
f = null; // 闭包死亡(包含闭包的函数对象成为垃圾对象)

闭包的应用:定义JS模块

JS模块:

​ 具有特定功能的js文件

​ 将所有的数据和功能都封装在一个函数内部(私有的)

​ 只向外暴露一个包含n个方法的对象或函数

​ 模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// myModule.js
function myModule() {
// 私有数据
var msg = 'My Module';
// 操作数据的函数
function doSomethiing() {
console.log('doSomething()', msg.toUpperCase());
}
function doOtherthing() {
console.log('doOtherthing()', msg.toLowerCase());
}

// 向外暴露对象(给外部使用的方法)
return {
doSomethiing: doSomethiing,
doOtherthing: doOtherthing
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- index.html -->
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
</body>
<script type="text/javascript" src="js/myModule.js"></script>
<script type="text/javascript">
var module = myModule();
module.doSomethiing();
module.doOtherthing();
</script>
</html>

另一种,将要暴露的内容赋给window。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function myModule2() {
// 私有数据
var msg = 'My Module';
// 操作数据的函数
function doSomethiing() {
console.log('doSomething()', msg.toUpperCase());
}
function doOtherthing() {
console.log('doOtherthing()', msg.toLowerCase());
}

// 向外暴露对象(给外部使用的方法)要暴露的对象给window
window.myModule2 = {
doSomethiing: doSomethiing,
doOtherthing: doOtherthing
}
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- index.html -->
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
</body>
<script type="text/javascript" src="js/myModule2.js"></script>
<script type="text/javascript">
myModule2.doSomethiing();
myModule2.doOtherthing();
</script>
</html>

闭包的缺点

1、缺点

​ 函数执行之后,函数内的局部变量没有释放,占用内存时间会变长

​ 容易造成内存泄露

2、解决

​ 能不用闭包就不用

​ 及时释放

1
2
3
4
5
6
7
8
9
10
11
12
<script type="text/javascript">
function fn1() {
var arr = new Array[10000000];
function fn2() {
console.log(arr.length);
}
return fn2;
}
var f = fn1();
f();
f = null; // 让内部函数成为垃圾对象-->回收闭包
</script>

补充

内存溢出

一种程序运行出现的错误

当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误

内存泄露

占用的内存没有及时释放

内存泄露累计多了就容易导致内存溢出

常见的内存泄露:

​ 意外的全局变量

​ 没有及时清理的计时器或回调函数

​ 闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 意外的全局变量
function fn() {
a = new Array(100000000); // 没有使用var定义局部变量,导致a变为意外的全局变量。
}
fn();

// 没有及时清理的计时器或回调函数
var intervalId = setInterval(function() {
console.log('------------');
}, 1000); // 使用完计时器没有清理
// clearInteval(intervalId);

// 闭包
function fn1() {
var a = 4;
function fn2() {
console.log('---', a);
}
return fn2;
}
var f = fn1();
f(); // 使用完闭包没有清理
// f = null;

五、面向对象高级

对象创建模式

方式1:Object构造函数模式

套路:先创建空Object对象,再动态添加属性/方法

适用场景:起始时不确定对象内部数据

问题:语句太多

1
2
3
4
5
6
7
8
9
var p = new Object();
p.name = 'Tom';
p.age = 12;
p.setName = function(name) {
this.name = name;
}

p.setName('JACK');
console.log(p.name, p.age);

方式2:对象字面量模式

套路:使用 {} 创建对象,同时指定属性/方法

适用场景:起始时对象内部数据是确定的

问题:如果创建多个对象,有重复代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var p = {
name: 'Tom',
age: 12,
setName: function(name) {
this.name = name;
}
}

// 测试
console.log(p.name, p.age);
p.setName('JACK');
console.log(p.name, p.age);

var p2 = { // 如果创建多个对象代码很重复
name: 'Bob',
age: 13,
setName: function(name) {
this.name = name;
}
}

方式3:工厂模式

套路:通过工厂函数动态创建对象并返回

适用场景:需要创建多个对象

问题:对象没有一个具体类型,都是Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createPerson(name, age) {
var obj = {
name: name,
age: age,
setName: function(name) {
this.name = name;
}
}
return obj;
}

// 创建两个对象
var p1 = createPerson('Tom', 12);
var p2 = createPerson('Bob', 13);

function createStudent(name, price) {
var obj = {
name: name,
price: price
}
return obj;
}
var s = createStudent('张三', 12000);
// 这里 p1、p2、s也都是Object类型。所以对象没有一个具体的类型。

方法4:自定义构造函数模式

套路:自定义构造函数,通过new创建对象

适用场景:需要创建多个类型确定的对象

问题:每个对象都有相同的数据,浪费内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义类型
function Person(name, age) {
this.name = name;
this.age = age;
this.setName = function(name) {
this.name = name;
}
}
var p1 = new Person('Tom', 13);
p1.setName('Jack');
console.log(p1.name, p1.age);
console.log(p1 instanceof Person);// true

function Studen(name, price) {
this.name = name;
this.price = price;
this.setName = function(name) {
this.name = name;
}
}
var s1 = new Studen('KC', 1200);
s1.setName('KAKA');
console.log(s1.name, s1.price);
console.log(s2 instanceof Studen);// true 这里和绗棉的p1就分别有了自己的类型

方式5:构造函数+原型的组合模式

套路:自定义构造函数,属性在函数中初始化,方法添加到原型上

适用场景:需要创建多个类型确定的对象

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.setName = function(name) { // 将方法放到原型中
this.name = name;
}

var p1 = new Person('Tom', 12);
var p2 = new Person('Bob', 13);
p1.setName('JACK'); // p1与p2都有各自的属性,而方法在原型上,他们的原型都是同一个,所以公用了一个方法

原型链继承

套路:

​ 定义父类型构造函数

​ 给父类型的原型添加方法

​ 定义子类型的构造函数

​ 创建父类型的对象赋值给子类型的原型

​ 将子类型原型的构造函数属性设置位子类型

​ 给子类型原型添加方法

​ 创建子类型的对象:可以调用父类型的方法

关键:

​ 子类型的原型为父类型的一个实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 父类型
function Supper() {
this.supProp = 'Supper property';
}
Supper.prototype.showSupperProp = function() {
console.log(this.supProp);
}

// 子类型
function Sub() {
this.subProp = 'Sub property';
}

// 子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper();
Sub.prototype.showSubProp = function() {
console.log(this.subProp);
}
var sub - new Sub();
sub.showSupperProp();
sub.showSubProp();

借用构造函数继承(假)

套路:

​ 定义父类型构造函数

​ 定义子类型构造函数

​ 在子类型构造函数中调用父类型构造

关键:

​ 在子类型构造函数中调用call() 调用父类型构造函数

1
2
3
4
5
6
7
8
9
10
11
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.price = price;
}

var s = new Student('Tom', 20, 10000);
console.log(s.name, s.age, s.price);

*组合继承

原型链+借用构造函数的组合继承

利用原型链实现对父类型对象的方法继承

利用supper()借用父类型构建函数初始化相同属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.setName = function(name) {
this.name = name;
}

function Student(name, age, price) {
Person.call(this, name, age); // 为了得到父类型属性
this.price = price;
}
Student.prototype = new Person(); // 为了能看到父类型的方法
Student.prototype.constructor = Student; // 修正contructor属性
Student.prototype.setPrice = function(price) {
this.price = price;
}

// test
var s = new Student('Tom', 24, 15000);
s.setName('Bob');
s.setPrice(11100);
console.log(s.name, s.age, s.price);

六、线程机制与事件机制

进程与线程

进程(process)

程序的一次执行,它占有一片独有的内存空间

可以通过window任务管理器查看进程

线程(thread)

线程是进程内的一个独立执行单元

是程序执行的一个完整流程

是CPU的最小的调度单元

相关知识

应用程序不许运行在某个进程的某个线程上

一个进程中至少有一个运行的线程:主线程,进程启动后自动创建

一个进程中可以同时运行多个线程,我们会说程序是多线程运行的

一个进程内的数据可以供其中的多个线程直接共享

多个进程之间的数据是不能直接共享的

线程池(thread pool):保存多个线程对象的容器,实现线程对象的反复利用

相关问题

何为多进程与多线程?

​ 多进程运行:一个应用程序可以同时启动多个实例运行

​ 多线程:一个进程内,同时多个线程运行

单线程与多线程

​ 多线程:

​ 优点:

​ 能有效提升CPU的利用率

​ 缺点:

​ 创建多线程开销

​ 线程间切换开销

​ 死锁与状态同步问题

​ 单线程:

​ 优点:顺序编程简单易懂

​ 缺点:运行效率低下

JS是单线程还是多线程?

​ JS是单线程运行的

​ 但是通过H5中的Web Wokers可以多线程运行

浏览器运行时多线程还是单线程?

​ 多线程

浏览器运行时单进程还是多进程?

​ 有的时单进程:

​ firefox、老版本IE

​ 有的时多进程:

​ chrome、新版IE

​ 可以通过程序运行时查看window的任务管理器判断是多进程还是单进程

浏览器内核

浏览器内核是支撑浏览器运行的最核心程序

不同的浏览器可能不一样,例如chrome、Safrai使用webkit,firefox使用Gecko,IE使用Trident,360、搜狗等国内浏览器使用Trident+webkit

内核由很多模块组成:

​ 主线程:

​ js引擎模块:负责js程序的编译与运行

​ html、css文档解析模块:负责页面文本的解析

​ DOM/CSS模块:负责dom/css在内存中的相关处理

​ 布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)

​ 分线程:

​ 定时器模块:负责定时器的管理

​ DOM事件响应模块:负责事件的管理

​ 网络请求模块:负责ajax请求

​ 其它模块:……

定时器引发的思考

定时器真的是定时执行吗?

​ 定时器并不能保证真正定时执行

​ 一般会延迟一丁点(可接受范围内),但是也可能延迟很长事件(不可接受范围)

定时器回调函数是在分线程执行的吗?

​ 在主线程执行的,js是单线程的

定时器是如何实现的?

​ 事件循环模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<button id='btn'>
启动定时器
</button>
<script type="text/javascript">
document.getElementById('btn').onclick = function() {
var start = Date.now();
console.log('启动定时器前...')
setTimeout(function() {
console.log('定时器执行了', Date.now() - start);
}, 200);
console.log('启动定时器后...')

// 以下做一个长时间的工作会使定时器延迟很长(不可接受范围)
for(var i = 0; i < 1000000; i++) { // 通过长时间循环延长定时器执行

}
}
</script>

JS是单线程的

1、如何证明js执行是单线程的?

setTimeout()的回调函数是在主线程执行的

定时器回到函数只有在运行栈中的代码全部执行完后才有可能执行

2、为什么js要用单线程模式,而不是多线程模式?

JavaScript的单线程,与它的用途有关

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以操作DOM

这决定了它只能是单线程,否则会带来复杂的同步问题

3、代码的分类

初始化代码

回调代码

4、js引擎执行代码的基本流程

先执行初始化代码:包含一些特别的代码 回调函数(异步执行)

​ 设置定时器

​ 绑定事件监听

​ 发送ajax请求

后面在某个时刻才会执行回调代码

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
setTimeout(function() {
console.log('timeout 2000')
}, 2000);
setTimeout(function() {
console.log('timeout 1000');
alert('1000');
}, 1000);
setTimeout(function() {
console.log('timeout 0');
}, 0);
function fn() {
console.log('fn()');
}
fn();

console.log('alert()之前');
alert('--------'); // 暂停当前主线程的执行,同时暂停计时,点击确定后,恢复程序执行和计时器
console.log('alert()之后');
/*
执行顺序:
fn()
alert()之前
弹出alert,点击确定
alert()之后
timeout 0
一秒后:timeout 1000
弹出alert 1000,点击确定
一秒后:timeout 2000
*/

事件循环模型

1、所有代码分类

初始化执行代码(同步代码):包含绑定dom事件监听,设置定时器,发送ajax请求的代码

回调执行代码(异步代码):处理回调逻辑

2、js引擎执行代码的基本流程:

初始化代码===>回调代码

3、模型的两个重要组成部分:

事件(定时器/DOM事件/Ajax)管理模块

回调队列

4、模型的运转流程

执行初始化代码,将事件回调函数交给对应模块管理

当事件发送时,管理模块会将函数及其数据添加到回调队列中

只有当初始化代码执行完后(可能要一定时间),才会遍历读取回调队列中的回调函数执行

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
<button id='btn'>
测试
</button>
<script type="text/javascript">
function fn1() {
console.log('fn1()');
}
fn1();
document.getElementById('btn').onclick = function() {
console.log('点击了btn');
}
setTimeout(function() {
console.log('定时器执行了');
}, 2000);
function fn2() {
console.log('fn2()');
}
fn2();
</script>
<!--
执行顺序:
fn1()
fn2()
定时器与按钮的先后顺序不确定
-->

Web Workers

H5规范提供了js分线程的实现,取名为:Web Workers

相关API:

​ Worker:构造函数,加载分线程执行的js文件

​ Worker.prototype.onmessage:用于接收另一个线程的回调函数

​ Worker.prototype.postMessage:向另一个线程发送消息

不足:

​ Worker内代码不能操作DOM(更新UI)

​ 不能跨域加载JS

​ 不是每个浏览器都支持这个新特性

Web Workers实际应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 使用递归计算所输入数字所在斐波那契数列的值 -->
<!-- 此计算方法当计算位数较大时则会使整个界面不可操作等待计算结果。主线程一直在计算中 -->
<input type="text" placeholder="数值" id="number" />
<button id='btn'>
计算
</button>
<script type="text/javascript">
function fibonacci(n) {
return n <= 2 ? 1 :fibonacci(n-1) + fibonacci(n-2); // 递归调用
}
var input = document.getElementById('number');
document.getElementById('btn').onclick = function() {
var number = input.value;
var result = fibonacci(number);
alert(result);
}
</script>

使用Web Workers解决方案如下:(需要在容器中启动才能通过测试)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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('myWorker.js');
// 向分线程发送消息
worker.postMessage(number);
// 绑定接收消息的监听
worker.onmessage = function (event) {
console.log('主线程接收封线程返回的数据', event.data);
alert(event.data);
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// myWorker.js
function fibonacci(n) {
return n <= 2 ? 1 : fibonacci(n-1) + fibonacci(n-2); // 递归调用
}

var onmessage = function(event) {
var number = event.data;
console.log('分线程接收到主线程发送的数据:', number);
// 计算
var result = fibonacci(number);
postMessage(result);
// alert(result); alert是window的方法,在分线程中不能调用
// console是浏览器实现的所以可以使用
// 分线程中有自己的全局对象
// 分线程中的全局对象不再是window,所以在分线程中不可能跟新界面
}

workers不足:

1、速度慢
2、不能跨域加载js
3、不是每个浏览器都支持这个新特性
4、worker内代码不能访问DOM(更新