尚硅谷JavaScript高级教程(javascript实战进阶)
1.基础总结深入
1.1数据类型
两大类:1.基本(值)类型 2.对象(引用)类型
基本(值)类型
String:任意字符串
Number:任意数字
Symbol:代表创建后独一无二且不可变的数据类型【它的出现我认为是为了解决可能出现的全局变量冲突的问题】
Boolean:true / false
Undefined:undefined
Null:null对象(引用)类型
Object:任意对象
Function:一种特别的对象(特别在可以执行)
Array:一种特别的对象(特别在属性为数值下标,内部数据是有序的)判断数据类型
- typeof : 可以判断:undefined / 数值 / 字符串 / 布尔值
- typeof : 不可以判断:null与object、object与array
- instanceof :判断对象的具体类型
- === : 可以判断:undefined / null (因为这两者只有一个数值)
1 | // 1.基本数据类型 |
1 | // 2.对象数据类型 |
为什么b1.b3 instanceof Object
是 true
,这是因为函数是一种特殊的对象。
对于上述定义中的b2
的第三个元素console.log
进行类型判断。
1 | console.log(typeof b1.b2[2] === 'function');//true |
1.undefined与null的区别是什么?
- undefined 代表定义未赋值
- null表示定义了变量并且赋值为null
2.什么时候给变量赋值为null?
- 初始赋值为null,表明将要赋值为对象。
- 结束前,让对象成为垃圾对象(被垃圾回收器回收)
1 | // 起始 |
3.严格区别变量类型与数据类型?
数据的类型:*基本类型 *对象类型
变量类型(变量内存值的类型):
*基本类型:保存基本类型的数据
*引用类型:保存地址值
对于上图中的变量a
和b
,因为其存储的是基本类型,所以数据直接保存在栈内存中,而对于变量c
而言,其代表对象,因此在栈内存中只存储该对象在堆内存中的地址。
1.2 数据,变量与内存
1.2.1 什么是数据?
● 存储在内存中的代表特定信息的东西,本质上是0101……
● 数据的特点:可传递、可运算
● 一切皆数据
● 内存中所有操作的目标:数据【*算术运算 、 *逻辑运算、 赋值运算 、运行函数】
1.2.2 什么是内存?
● 内存条通电之后产生的可存储数据的空间(临时的);
● 内存的产生和死亡:内存条(电路板) → 通电 → 产生内存空间 → 存储数据 → 处理数据 → 断电 → 内存空间和数据都消失;
● 一块小内存可以保存的2种数据:内部存储的数据 + 地址值
● 内存的分类:
栈:全局变量和局部变量(空间较小)
堆:对象(空间较大)
1 | var obj = {name:'Tom'}; |
上述代码在打印时,首先读取的是obj
,此时读取的是obj
的内容值,只不过内容值当中存储的是obj
的地址值。
1.2.3 什么是变量?
可变化的量,由变量名和变量值组成;
每个变量都对应一小块内存,变量名用来查找对应的内存,变量值就是内存中保存的数据
1.2.4 内存、数据、变量三者之间的关系?
● 内存:用来存储数据的的空间
● 变量:是内存的标识
1.2.5 相关问题:
⭐ var a = xxx,a内存中到底保存的是什么?
- xxx是基本数据,保存的是这个数据
- xxx是对象,保存的是对象的地址值
- xxx是一个变量,保存的是xxx的内容值(可能是基本数据,也可能是地址值)
⭐ 关于引用变量赋值问题?
● n个引用变量指向同一个对象,通过一个变量修改对象内部的数据,其他所有变量看到的是修改之后的数据。
● 2个引用对象指向同一个对象,让其中一个引用变量指向另一个对象,另一个引用变量依然指向前一个对象
1 | var obj1 = { name:'Tom' }; |
上述代码中,实现了将obj1
的内容保存的obj2
【obj1
中保存的是地址值】
1 | var obj1 = { name: 'Tom' }; |
上述第二部分代码图示:
代码执行完毕之后,age:15
会在哪里?
变量obj
是函数的局部变量,在函数执行完毕之后,局部变量会被释放,一旦局部变量obj
被释放,那么其指向的age:15
就会被当作垃圾对象回收。
1 | function fn3(obj){ |
实际上只要是没有切断变量与堆内存中存储内容之间的执行,修改其中一个,其他都会跟着变。
⭐ 在JS调用函数时传递变量参数,是值传递还是引用传递?
答案:
- 理解1:都是值传递,值分为两种:基本值和地址值。
- 理解2:可能是值传递,也可能是引用传递(地址值)。
1 | var a = 3; |
⭐ JS引擎如何管理内存?
1.内存生命周期
- 分配小内存空间,得到它的使用权
- 存储数据,可以反复进行操作
- 释放小内存空间
2.释放内存
- 局部变量:函数执行完自动释放
- 对象:成为垃圾对象,由垃圾回收器回收
1 | var a = 3; |
上述代码中共使用几个内存空间?
3个内存空间。一个是栈内存a,存储数据3;一个是栈内存obj,存储数据{}(堆内存)的地址;另一个是堆内存中存储的{}。
1 | var a = 3; |
上述代码中共使用几个内存空间?
2个内存空间。一个是栈内存a,存储数据3;一个是栈内存obj,存储值为null。堆内存中存储的{}被回收。
1 | function fn(){ |
上述代码中的b在函数fn()
执行完毕之后会自动释放,而b指向的对象是在后面的某个时刻由垃圾回收器回收。
1.3 对象
1.3.1 什么是对象?
多个数据的封装体 或者 是用来保存多个数据的容器;
一个对象代表现实中的一个事物
1 | var person = { |
1.3.2 为什么用对象?
统一管理多个数据
1.3.3 对象的组成
属性 + 方法
♥ 属性:属性名(字符串)和属性值(任意类型)组成
♥ 方法:是一种特别的属性(属性值是函数)
1 | var person = { |
输出结果:
1.3.4 如何访问对象内部数据?
※ 第一种方式:.属性名
,编码简单,有时不能用
1 | p.setName('Bob'); |
※ 第二种方式:['属性名']
,编码麻烦,能通用
1 | person['setAge'](23); |
⭐ 什么情况下必须使用['属性名']
的方式?
1.属性名包含特殊字符:- 、空格
2.属性名不确定(属性名是个变量)
1 | var p = {} |
1.4 函数
1.4.1 什么是函数?
实现特定功能的n条语句的封装体
只有函数是可以执行的,其他类型的数据不能执行
1.4.2 为什么要用函数?
提高代码复用、便于阅读交流
1.4.3 如何定义函数?
函数声明
表达式
1 | 1.函数声明 |
1.4.4 如何调用(执行)函数?
直接调用:test()
通过对象调用:obj.test()
new调用:new test()
临时让test成为obj的方法进行调用:test.call(obj)
、 test.apply(obj)
1 | 举例: |
1.4.4 回调函数
什么是回调函数?
3个特点:
1)自己定义的
2)没有调用
3)最终执行了(在某个时刻 或者 某个条件下)
常见的回调函数
- dom事件的回调函数 ➡ this指的是发生事件的dom元素
- 定时器回调函数 ➡ this指的是window
- ajax请求回调函数
- 生命周期回调函数
1.4.5 IIFE
Immediately-Invoked Function Expression,立即调用函数表达式
匿名函数自调用 = IIFE
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
1 | // 匿名函数自调用 |
♥ 作用:
- 隐藏实现
- 不会污染外部(全局)命名空间
- 用它来编写js模块
1 | (function () { |
1.4.6 函数中的this
this 是什么?
- 任何函数本质上都是通过某个对象来调用的
- 所有函数内部都有一个变量this
- 它的值是调用函数的当前对象
如何确定this的值?
test()
:windowp.test()
:pnew test()
:新创建的对象p.call(obj)
:obj
- 函数调用时,指向window
- 以方法调用时,指向调用该方法的对象
- 使用new创建一个对象时,指向新创建的对象
- call,apply ,bind可以改变this指向,this指向指定的那个对象
- 在全局作用域中this代表window
2.函数高级
2.1原型与原型链
2.1.1 原型(prototype)
1.函数的prototype
属性
- 每个函数都有一个
prototype
属性,它默认指向一个Object
空对象(即称为:原型对象)何为Object空对象? 没有我们自己定义的属性
- 原型对象中有一个属性
constructor
,它指向函数对象1
2console.log(Date.prototype.constructor === Date); // true
console.log(fun.prototype.constructor === fun); //true(fun为自定义函数)
⭐ 构造函数和它的原型对象是一个相互引用的关系。
解释相互引用:当前存在两个对象A
和B
,A
当中有一个属性可以找到B
,B
当中有一个属性可以找到A
。
2.给原型对象添加属性(这里的属性一般都是方法)
给原型对象添加属性(一般是方法)➡ 实例对象可以访问
1 | function Fun(){ |
2.1.2 显式原型与隐式原型
- 每个函数
function
都有一个prototype
,即显式原型(属性) - 每个实例对象都有一个
__proto__
,可称为隐式原型(属性) - 对象的隐式原型的值为其对应构造函数的显式原型的值
- 总结:
- 函数的
prototype
属性:在定义函数时自动添加的,默认为一个空Object对象 - 对象的
__proto__
属性:在创建对象时自动添加的,默认值为构造函数的prototype
属性值 - 程序员能直接操作显式原型,但不能直接操作隐式原型(ES6之前)
1 | function Fn () { // 内部语句:this.prototype = {} |
上述代码内存结构图:
2.1.3 原型链
访问一个对象的属性时:
- 先在自身属性中查找,如果找到则返回
- 如果未找到,再沿着
__proto__
这条链向上查找,找到返回 - 如果最终没有找到,返回
undefined
原型链的别名是隐式原型链。
原型链的作用:查找对象的属性(方法),只是用来查找,如果是一些赋值操作等,则不会查找原型链。
原型链的本质是隐式原型链。
原型链的尽头是Object的原型对象
。
1 | function Fn () { |
原型链的尽头是Object的原型对象
。
创建两个实例对象,实例对象有隐式原型属性__proto__
,这个隐式原型属性指向的是Object的原型对象,(隐式原型属性__proto__
的值是将prototype
的值赋给它得到的)。
所有函数的
__proto__
都是一样的,都是通过new Function()
产生的任何函数都是通过
new Function()
产生的.因此,所有函数对象的隐式原型都指向Function.prototype
实例对象的隐式原型属性等于构造函数的显示原型属性
小结:
函数的显式原型指向的对象:默认是空Object实例对象【但是Object不满足】
1
2
3
4
5
6
7
8
9function Fn () {
this.test1 = function () {
console.log('test1()');
}
}
console.log(Fn.prototype instanceof Object); // true
console.log(Object.prototype instanceof Object); //false
console.log(Function.prototype instanceof Object); // true
( 😀 因此,上图中存在绿色部分)所有函数都是Function的实例,包括Function它自身。
1
console.log(Function.__proto__ === Function.prototype); // true
Object 的原型对象是原型链的尽头
1
console.log(Object.prototype.__proto__); // null
原型链的属性问题
- 读取对象属性值时:会自动到原型链中查找
- 设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加属性并设置其值
- 方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
※ 原型链是用来查找属性的,当为一个对象添加属性时,不会看原型链
1 | function Fn () { |
此时fn1
的内容为:
⭐ 查找fn1
的a
属性值时,发现fn1
本身没有属性a
,所以会自动到原型链中查找属性a
。
此时fn2
的内容为:
⭐ 查找fn2
的a
属性值时,发现fn2
本身有属性a
,所以直接输出该属性值,不再去查找原型链,但是原型链中实际上仍存在属性a
。
1 | function Person (name, age) { |
⭐ 方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
1 | var p2 = new Person('Lily', 13) |
⭐ 实例对象的隐式原型等于构造函数的显式原型
2.1.4 instanceof
instanceof 作用:a instanceof b
→ 判断a
是否是b
的实例【其中,a是实例对象,b是构造函数】
- instanceof 是如何判断的?
表达式:A instanceof B
如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false
1 | function Foo () { |
1 | console.log(Object instanceof Function); // true |
2.1.5 面试题
题目1
1 | function A () { |
上述代码在内存中的示意图:
(最开始是是红色指示线,后来变为蓝色指示线)
两种表达方式:
1 | var B = function(){} |
其中表达方式1会影响原有的实例对象,因为表达方式1是在原有的实例对象上添加到。
表达方式2不会影响原有的实例对象,因为表达方式2直接改变了实例对象原型。
对于代码
1 | A.prototype = { |
的理解:现在堆内存中创建对象{n:2,m:3}
,然后改变A.prototype
的指向。(蓝色指示线部分)
题目2
1 | var F = function () { |
原型链一张图
实记口诀:
- 函数都是
Function
构造出来的 - 一切函数都是对象,只要是函数对象,就会有原型
prototype
和隐式原型__proto__
两个属性 - 普通对象身上只有
__proto__
,没有prototype
- 实例化对象的
__proto__
都指向构造函数的prototype
- 所有函数的
prototype
都指向自身的prototype
- 所有的
prototype
的__proto__
都指向Object.prototype
(Object除外) - 所有函数对象的
__proto__
都指向Function.prototype
(包括Function自身) - 所有对象身上都有
constructor
指向函数自身
2.2执行上下文与执行上下文栈
2.2.1 变量提升与函数提升
1 | // 面试题:程序最终输出什么? |
上述代码的实际执行过程是
1 | function fn(){ |
1 | console.log(b); //undefined |
上述代码在执行console.log
时,变量b
和函数fn2
均未提前声明,但是仍然可以执行。原因是变量b
进行了变量提升,函数fn2
进行了函数提升。
- 变量(声明)提升
通过var
定义(声明)的变量,在定义语句之前就可以访问到,值为undefined
- 函数(声明)提升
通过function
声明的函数,在之前就可以直接调用,值为函数定义。
函数提升必须使用声明的方式。结果为:1
2
3
4
5console.log(fn2);
function fn2 () {
console.log('fn2');
}
1 | console.log(fn3); //undefined |
上述代码遵循的是变量提升。
2.2.2 执行上下文
1.代码分类
全局代码 和 函数(局部)代码
2.全局执行上下文
在执行全局代码前将window
确定为全局执行上下文。
对全局数据进行预处理:
var
定义的全局变量 →undefined
,添加为window
的属性function
声明的全局函数 → 赋值(fun
),添加为window
的方法this
→ 赋值(var
)
–上述过程是在全局代码执行之前就会进行的操作
- 执行全局代码
3.函数执行上下文
在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
对局部数据进行预处理:
- 形参变量 → 赋值(实参) → 添加为执行上下文的属性
arguments
→ 赋值(实参列表),添加为执行上下文的属性var
定义的局部变量 → undefined,添加为执行上下文的属性function
声明的函数 → 赋值(fun),添加为执行上下文的方法this
→ 赋值(调用函数的对象)
开始执行函数体代码
2.2.3 执行上下文栈
- 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
- 在函数执行上下文创建后,将其添加到栈中(压栈)
- 在当前函数执行完成后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后,栈中只剩下window
处于活动状态的执行上下文环境只有一个。
1 | // 1.进入全局执行上下文 |
上述代码结果为30。
执行上下文栈结构图如下所示:
栈的底部始终是window
,因为第一个产生的是window
,要放入栈中进行管理。
执行上下文个数遵循 n + 1
的原则,其中n
指的是调用函数的次数,1
指的是window
。
面试题
⭐ 1.依次输出什么?整个过程中产生了几个执行上下文?
1 | console.log('global begin:' + i); |
依次输出的结果:
global begin:undefidned
foo begin:1
foo begin:2
foo begin:3
foo end:3
foo end:2
foo end:1
global end:1
整个过程中产生了5个执行上下文。
⭐ 2.函数提升与变量提升顺序
1 | function a () { } |
上述代码先执行函数提升,再执行变量提升,但是变量未进行赋值操作,所以是function。
具体参考:变量提升和函数提升的优先级问题
1 | if (!(b in window)) { |
1 | var c = 1; |
上述代码相当于:
2.3作用域与作用域链
2.3.1 作用域
- 理解
作用域就像是一块“地盘”,一个代码所在的区域。它是静态的(相对于 执行上下文对象 而言,因为执行上下文对象只有代码执行时才会产生),在编写代码时就确定了。 - 分类
全局作用域、函数作用域、块级作用域(ES6出现) - 作用
隔离变量,不同作用域下同名变量不会有冲突 - 作用域个数
作用域个数遵循n + 1
的原则(与执行上下文相同),其中n
指的是定义的函数的个数,1
指的是全局作用域。
结果为:
fn() 100 20 300 10
bar() 1000 20 300 400 100
bar() 1000 20 300 400 200
2.3.2 作用域与执行上下文
- 区别:
⭐全局作用域之外,每个函数都会创建自己的作用域。作用域在函数定义时就已经确定了,而不是在函数调用时。全局执行上下文环境是在全局作用域确定之后,JS代码马上执行之前创建;函数执行上下文环境是在调用函数时,函数代码执行之前创建。
⭐作用域是静态的,只要函数定义好了就一直存在,且不会再变化。执行上下文是动态的,调用函数时创建,函数调用结束时就会自动释放。 - 联系
执行上下文(对象)是从属于所作用域。全局上下文环境 → 全局作用域;函数上下文环境 → 对应的函数作用域。
2.3.3 作用域链
- 理解
多个上下级关系的作用域形成的链,它的方向是从下向上(从内到外);查找变量时就是沿着作用域链来查找的 - 查找一个变量的查找规则
① 在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入②;
② 在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入③;
③ 在执行②的相同操作,直到全局作用域,如果还找不到就排除找不到的异常。
1 | var a = 1; |
结果:
2.3.4 面试题
⭐
1 | var x = 10; |
执行函数fn()
时,会先在fn
的作用域中查找x
,没有找到后会跳到其外部的作用域中继续寻找,fn
的作用域和show
的作用域是同级的不会相互查找。
⭐
1 | var fn = function () { |
结果为:
报错的原因是
在函数作用域中查找不到fn2
,会到全局作用域中继续查找,在全局作用域中不存在fn2
。
※ 如果想要调用fn2
,应该修改代码为
1 | var obj = { |
2.4闭包
如何产生闭包?
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包。闭包是什么?
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。使用chrome调试查看:
※ 理解一:闭包是嵌套的内部函数(该函数中引用了外部函数的变量)
※ 理解二:包含被引用变量(函数)的对象
注意:闭包存在于嵌套的内部函数中产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量 \ 函数)
- 执行了外部函数
2.4.1 常见的闭包
- 将函数作为另一个函数的返回值结果为
1
2
3
4
5
6
7
8
9
10function fn1 () {
var a = 2;
function fn2 () {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
console.log(f);
上述过程中创建了1个闭包。通过代码fn1()
产生。
再执行下列代码:1
2f(); // 3
f(); // 4 - 将函数作为实参传递给另一个函数调用上述代码在执行过程中产生了闭包。因为
1
2
3
4
5
6
7function showDelay (msg, time) {
setTimeout(function () {
console.log(msg)
}, time)
}
showDelay('延迟输出', 2000)
嵌套的内部函数引用了外部函数的变量msg
若将代码修改如下,就不会产生闭包1
2
3
4
5
6
7function showDelay (msg, time) {
setTimeout(function () {
console.log('无闭包产生');
}, time)
}
showDelay('延迟输出', 2000)
2.4.2 闭包的作用
使得函数内部的变量在函数执行完后,仍存活在内存中(延长了局部变量的生命周期)
让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
※ 1. 函数执行完后,函数内部声明的局部变量是否还在?
一般是不存在,存在于闭包中的变量才可能存在
※ 2. 在函数外部能直接访问函数内部的局部变量吗?
不能,但是通过闭包可以让外部操作它
2.4.3 闭包的生命周期
- 产生:在嵌套内部函数定义执行完时就产生了(不是在调用时)
- 死亡:在嵌套的内部函数成为垃圾对象时
1 | function fn1 () { |
2.4.3 闭包的应用:定义JS模块
JS模块是具有特定功能的JS文件,将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或者函数,模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能。
※ 方式一
1 | function myModule () { |
※ 方式二
1 | (function () { |
2.4.4 闭包的缺点及解决
- 缺点:
■ 函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
■ 容易造成内存泄漏 【内存泄漏:内存占用却不使用】 - 解决:
■ 能不用闭包就不用
■ 及时释放
1 | function fn1 () { |
补充知识点:内存溢出与内存泄漏
2.4.5 面试题
⭐ 题目 1
1 | var name = 'This Window'; |
输出的结果为This Window
,原因是函数调用时,this
指向window
,因此this.name
指的是全局变量name
的值。
1 | var name = 'This Window'; |
输出的结果为My Object
,原因是this
是在对象object
的方法getNameFunc
中进行调用的,以方法调用时,this
指向调用该方法的对象,因此that.name
相当于object.name
。
⭐ 题目 2
1 | function fun (n, o) { |
结果为:
1 | undefidned 0 0 0 |
3.面向对象高级
3.1 对象创建模式
⭐ 方式一:Object构造函数模式
- 套路:先创建空
Object
对象,再动态添加属性/方法 - 使用场景:起始时不确定对象内部数据
- 问题:语句太多
1 | // 一个人:name:"Tom",age:12 |
结果为
⭐ 方式二:对象字面量模式
- 套路:使用{}创建对象,同时指定属性/方法
- 使用场景:起始时对象内部数据是确定的
- 问题:如果创建多个对象,有重复代码
1 | // 一个人:name:"Tom",age:12 |
⭐ 方式三:工厂模式
- 套路:通过工厂函数动态创建对象并返回
- 适用场景:需要创建多个对象
- 问题:对象没有一个具体的类型,都是object类型
※ 何为工厂函数? —— 返回一个对象的函数成为工厂函数
1 | function createPerson (name, age) { |
⭐ 方式四:自定义构造函数模式
- 套路:自定义构造函数,通过new创建对象
- 适用场景:需要创建多个类型确定的对象
- 问题:每个对象都有相同的数据,浪费内存
1 | function Person (name, age) { |
但是此种方式创建的对象,每个对象中都包含setName
方法,占用内存。
⭐ 方式五:构造函数+原型的组合模式
- 套路:自定义构造函数,属性在函数中初始化,方法添加到原型上
- 适用场景:需要创建多个类型确定的对象结果为:
1
2
3
4
5
6
7
8
9
10
11
12function 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('Jack', 13);
console.log(p1, p2)
3.2 原型链继承
3.2.1 方式一: 原型链继承
过程如下:
- 定义父类型构造函数
- 给父类型的原型添加方法
- 定义子类型的构造函数
- 创建父类型的对象赋值给子类型的原型
- 将子类型原型的构造属性设置为子类型
- 给子类型原型添加方法
- 创建子类型的对象:可以调用父类型的方法
⭐ 关键点:子类型的原型为父类型的一个实例对象
1 | 1. 定义父类型构造函数 |
3.2.2 方式二: 借用构造函数继承(假的)
过程如下:
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造
⭐ 关键点:在子类型构造函数中通过call()
调用父类型构造函数
1 | 1. 定义父类型构造函数 |
结果为:Student { name: 'Tom', age: 12, price: 13000 }
3.2.3 方式三: 组合继承(原型链 + 借用构造函数)
过程如下:
- 利用原型链实现对父类型对象的方法继承
- 利用
call()
借用父类型构造函数初始化相同属性
1 | function Person (name, age) { |
结果为Student { name: 'Jack', age: 12, price: 15000 }
3.2.4 补充:new一个对象背后做了什么?
- 创建一个空对象
- 给对象设置
__proto__
,值为构造函数对象的prototype
属性值 - 执行构造函数体(给对象添加属性 / 方法)
4.线程机制与事件机制
4.1 进程与线程
进程(process):程序的一次执行,它占有一片独有的内存空间。可以通过window任务管理器查看进程。
线程(thread):线程是进程内一个独立执行的单元,是程序执行的一个完整流程,是CPU的最小的调度单元。
如果一个程序有2个进程,每个进程包含1个线程,则该程序是单线程程序。
⭐ 相关知识:
- 应用程序必须运行在某个进程的某个线程上
- 一个进程至少有一个运行的线程:主线程(线程启动后自动创建)
- 一个进程中也可以同时运行多个线程,我们会说程序是多线程运行的
- 一个进程内的数据可以供其中的多个线程直接共享
- 多个进程之间的数据是不能直接共享的
- 线程池(thread pool):保存多个线程对象的容器,实现线程对象的反复利用
⭐ 相关问题:
- 何为多进程与多线程?
多进程运行:一个应用程序可以启动多个实例运行
多线程:在一个进程内,同时有多个线程运行 - 比较单线程与多线程?
多线程:【优点】能有效提升CPU的利用率 【缺点】创建多线程开销,线程间切换开销,死锁与状态同步问题
单线程:【优点】顺序变成简单易懂 【缺点】效率低 - JS是单线程还是多线程?
JS是单线程运行的。但使用H5中的Web Workers可以多线程运行。 - 浏览器运行是单线程还是多线程?
浏览器运行是多线程。 - 浏览器运行是单进程还是多进程?
有单进程也有多进程。其中,单进程:firefox、老版IE,多进程:chrome、Edge。4.2 浏览器内核
浏览器名称 | 内核 |
---|---|
Chrome、Safari | webkit |
firefox | Gecko |
IE | Trident |
360,搜狗等国内浏览器 | Trident + webkit |
浏览器内核由很多模块组成,其中包括:
- js引擎模块:负责js程序的编译与运行
- html,css文档解析模块:负责页面文本解析
- DOM/CSS模块:负责dom/css在内存中的相关处理
- 布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)
【上述部分运行在主线程】
- ……
- 定时器模块:负责定时器的管理
- DOM事件响应模块:负责事件管理
- 网络请求模块:负责ajax请求
【上述部分运行在分线程】
4.3 定时器引发的思考
相关问题:
- 定时器真的是定时执行的吗?
定时器并不能保证真正定时执行,一般会延迟一丁点(可以接受),也可能延迟很长时间(不能接收)多次执行结果为1
2
3
4
5var start = Date.now();
console.log('启动定时器前……');
setTimeout(function () {
console.log('定时器执行了', Date.now() - start);
}, 200)
1 | var start = Date.now(); |
多次执行结果为
定时器回调函数是在哪个线程执行的?
在主线程执行(JS是单线程的)定时器是如何实现的?
事件循环模型(详情见后续内容)
4.4 JS是单线程执行的
- 如何证明JS执行是单线程的?
setTimeout()的回调函数是在主线程执行的
定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行执行结果:1
2
3
4
5
6
7
8
9
10setTimeout(function () {
console.log('timeout --> 2000');
}, 2000)
setTimeout(function () {
console.log('timeout --> 1000');
}, 1000)
function fn () {
console.log('fn函数执行');
}
fn()
1 | setTimeout(function () { |
执行结果:
为什么JS要用单线程模式,而不用多线程模式?
Javascript的单线程与它的用途有关,作为浏览器脚本语言,Javascript的主要用途是与用户互动以及操作DOM,这决定了它只能是单线程的,负责会带来很复杂的同步问题。代码分类
- 初始化代码
- 回调代码
- JS引擎执行代码的基本流程
先执行初始化代码【包含一些特别的代码:设置定时器、绑定监听、发送ajax请求】,后面的某一个时刻才会执行回调代码
其中,设置定时器指的是setTimeout(),不包括内部的回调函数,其内部的回调函数需要在初始化代码执行完毕之后再执行
1 | setTimeout(function () { |
最终结果执行顺序:
4.5 事件循环(轮询)模型
4.5.1 模型原型图
对于JS代码可以分为两类:
- 初始化执行代码(同步代码):包含绑定dom事件监听,设置定时器,发送ajax请求的代码
- 回调执行代码(异步代码):处理回调逻辑
JS引擎执行代码的基本流程:初始化代码 ➡ 回调代码
模型的两个重要组成部分:事件管理模块、回调队列
🍎 模型的运转流程:执行初始化代码,将事件回调函数交给对应模块管理 ➡ 当事件发生时,管理模块会将回调函数及其数据添加到回调队列中 ➡ 只有当初始化代码执行完成后(可能需要一段时间),才会遍历读取回调队列中的回调函数执行
1 | function fn1 () { |
上述代码的执行结果不唯一,首先确定的结果是
1 | 执行fn1() |
剩余的执行了定时器
和点击了btn
,需要看点击btn的时机,如果点击btn在前,则执行顺序为点击了btn
、执行了定时器
,否则为执行了定时器
、点击了btn
。但是无论怎样执行fn1()
和执行fn2()
的顺序是不会改变的,因为这个为初始化执行代码。
4.5.2 相关概念
- 执行栈(execution stack):所有代码都是在此空间中执行的
- 浏览器内核(browser core):js引擎模块(在主线程)、其他模块(在主/分线程处理)
- 任务队列(task queue)、消息队列(message queue)、事件队列(event queue):同一个callback queue
- 事件轮询(event loop):从任务队列中循环取出回调函数放入执行栈中处理(一个接一个)
4.6 H5 Web Workers(多线程)
H5提供了JS分线程的实现,取名为:Web Workers
,我们可以将一些大计算量的代码交由web Worker 运行而不冻结用户界面。但是子线程完全受主线程控制,且不操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
相关API:
Worker
:构造函数,加载分线程执行的JS文件Worker.prototype.onmessage
:用于接收另一个线程的回调函数Worker.prototype.postMessage
:向另一个线程发送消息
不足之处:
Worker
内代码不能操作DOM(更新UI)- 不能跨域加载JS
- 不是每个浏览器都支持这个新特性
- 速度慢
1 | <input type="text" id="number"> |
1 | // work.js文件 |
结果:
主线程中的this
是window
,而work线程中的this
是一个专门为 Worker
定制的全局对象。
补充问题
JS分号问题
- JS一条语句的后面可以不加分号
- 是否加分号是编码风格问题,没有应该不应该,只有开发者喜欢与否
- 下面两种情况不加分号会报错
※ 小括号开头的前一条语句 (匿名函数自调用)
※ 中方括号开头的前一条语句⭐解决方法:在行首加分号1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
211.小括号开头的前一条语句 (匿名函数自调用)
var a = 3
;(function(){
console.log('Hello')
})()
错误理解:
var a = 3(function(){
console.log('Hello')
})()
---
2.中方括号开头的前一条语句
var a = 3
;[3,4].forEach(function(){
console.log('Hello')
})
错误理解:
var a = 3[3,4].forEach(function(){
console.log('Hello')
})