基础深入
数据类型
JS 中的数据类型分两大类
- 基本(值)类型
- 对象(引用)类型
基本(值)类型
String —— 任意字符串
Number —— 任意数字, 包括 NaN
Boolean —— true / false
Undefined —— undefined
Null —— null
引用(对象)类型
Object —— 任意对象都是 Object 类型的实例
- Function —— 一种特别的对象 (
可以执行
) - Array —— 一种特别的对象(
有数值下标, 内部数据是有序的
)
判断数据类型
typeof
- 返回数据类型的
字符串
- 判断 数值 / 字符串 / 布尔值 / undefined / 函数
- 不能判断 null
- 不能区分 object 和 array
1
2
3
4
5typeof a
typeof null // 'object'
typeof fun // 'function'
instanceof
- 返回
true / false
- 专门用来 判断对象 的具体类型的, 是不是
数组 / 普通对象 / 函数对象
1
2
3obj instanceof Object(一个构造函数名)
fun instanceof Function
- 专门用来 判断对象 的具体类型的, 是不是
===
- 返回
true / false
- 可以判断 undefined / null
1
2var a = null
console.log(a === null) // true
- 可以判断 undefined / null
基本问题?
undefined 和 null 的区别?
- undefined 代表声明了但是没有赋值
- null 代表赋值了, 值为 null
什么时候赋值为 null?
- 初始赋值为 null, 表明将要赋值为对象;
- 最后赋值为 null, 让对象成为垃圾对象(被回收)
严格区分变量类型和数据类型?
- 数据类型
- 基本数据类型
- 对象数据类型
- 变量类型(变量内存值的类型)
- 基本类型 —— 保存的就是
基本数据类型的数据
- 引用类型 —— 保存的是对象的
地址值
- 基本类型 —— 保存的就是
- 数据类型
数据 ? 内存 ? 变量 ?
数据
存储在内存中, 代表特定信息的, 本质上是 0101……
- 数据的特点
- 可传递
- 可运算
内存
可存储数据的空间(临时的)
- 内存的分类
- 栈内存
- 全局变量和局部变量
- 堆内存
- 对象内容
- 栈内存
变量
可变化的量, 由变量名和变量值组成
- 每个变量都对应一块小内存
- 变量名用来查找对应的内存; 变量值就是内存中保存的数据
内存、数据、变量三者之间的关系
- 数据
- 存储在内存中代表特定信息的东西, 本质是
01010...
- 存储在内存中代表特定信息的东西, 本质是
- 内存
- 用来存储数据的空间(临时的)
- 变量
- 内存的标识
几个问题?
- 赋值和内存的问题?
- var a = xx, a 内存中到底保存的是什么?
- 如果 xx 是基本数据, 保存的就是这个数据
- 如果 xx 是对象, 保存的是这个对象的地址值
- 如果 xx 是一个变量, 保存的就是 xx 的内存内容(可能是基本数据, 也可能是地址值)
- var a = xx, a 内存中到底保存的是什么?
- 引用变量赋值的问题?
- n 个引用变量指向同一个对象, 通过一个变量修改对象内部数据, 其他所有变量看到的是修改之后的数据
- 在 JS 调用函数时传递参数变量时, 是值传递还是引用传递?
- 可能是值传递, 也可能是引用传递(地址值)
- JS 引擎如何管理内存?
- 内存生命周期
- 分配内存空间, 得到它的使用权
- 存储数据
- 释放当前内存空间
- 释放内存
- 局部变量 —— 函数执行完自动释放
- 对象 —— 成为垃圾对象, 再由垃圾回收器回收
1
2
3
4
5
6
7
8
9// 常见面试题
var a = { age: 12 }
function fn (obj) {
obj = { age: 15 }
}
fn(a) // 调用 fn 函数时, 相当于执行了 obj = a; obj = { age: 15 }; a 并没有被改变
console.log(a.age) // 12
- 内存生命周期
对象
什么是对象?
- 一种复合的数据类型, 多个数据的封装体
- 用来保存多个数据的
容器
- 一个对象代表现实中的一个事物
为什么要用对象?
- 统一管理多个数据
对象的组成?
- 属性 —— 属性名(字符串) 和 属性值(任意类型) 组成
- 方法 —— 一种特别的属性, 属性值是
函数
如何访问对象内部数据?
对象.属性名
- 编码简单, 有时不能用
对象[属性名字符串]
- 能通用
- 如果是
常量属性名一定要加引号
- 如果是变量属性名则不能加引号
1
2
3
4
5
6
7
8var a = { name: 'zs' }
a.name // 'zs'
a['name'] // 'zs'
a[name] // undefined
var name = 'name'
a[name] // 'zs'
什么时候必须使用
对象[属性名字符串]
的方式?- 1.属性名包含特殊字符(-、空格等)
- 2.属性名不确定 —— 对象[变量名 ]
函数
什么是函数?
- 实现特定功能的代码段
- 只有函数是可以执行的, 其他类型的数据不能执行
为什么要使用函数?
- 提高代码复用
- 便于阅读交流
如何定义函数?
- 函数声明的方式
- 函数表达式的方式
如何调用(执行)函数?
- 函数名() —— 直接调用
- obj.fun() —— 通过对象调用
- new Fun() —— new 调用
- fun.call/apply(obj) —— 类似于 obj.fun(), 但是这
只是临时的使 fun 成为 obj 的方法
可以让一个函数成为任意指定对象的方法进行调用1
2
3
4
5
6
7var obj = {}
function test() {
this.xx = 'hello'
}
obj.test() // undefined is not a function
test.call(obj)
console.log(obj.xx) // 'hello'
回调函数
什么函数才是回调函数?
- 你定义的
- 你没有调
- 但最终它执行了
常见的回调函数?
- dom 事件回调函数
- 定时器回调函数
- ajax 请求回调函数
- 生命周期回调函数
IIFE
Immediately- Invoked Function Expression
- 匿名函数自调用
1
2
3
4// 匿名函数自调用
(function () {
console.log('haha')
})()
- 匿名函数自调用
作用
- 隐藏实现
- 不会污染全局命名空间
- 可以用来编写 js 模块
函数中的 this
- this 是什么?
- 函数本质都是通过某个对象来调用的, 所有函数内部都有一个变量 this
- 它的值是
调用函数的当前对象
- 如何确定 this 的值?
- fun() —— window
- p.fun() —— p
- new Person() —— 新创建的实例对象
- p.call(obj) —— obj
bind()
用来改变函数内部的 this
返回值 —— 改变 this 指向后的函数(可以预置参数)
1 | function fn (a, b, c) { |
总结:调用 bind 后的函数还是在调用原函数,只是改变了原函数的 this 而已
函数高级
原型与原型链
原型
每个函数都有一个 prototype 属性, 它默认指向一个 Object 空对象 (即称为原型对象)
- 原型对象中有一个属性 constructor, 它指向函数对象
- 给原型对象添加属性(一般是方法)
- 作用 —— 实例对象自动拥有原型中的属性(方法)
显示原型与隐式原型
每个函数都有一个 prototype
, 即显式原型(属性)
每个实例对象都有一个 __proto__
, 可称为隐式原型(属性)
对象的隐式原型的值为其对应构造函数的显式原型的值
- 总结:
- 函数的 prototype 属性 —— 在定义函数时自动添加的, 默认值是一个空 Object 对象
- 对象的
__proto__
属性 —— 创建对象时自动添加的, 默认值为其对应的构造函数的 prototype 属性值 - 程序员能直接操作 显示原型, 不能直接操作隐式原型(ES6 之前)
1
2
3
4
5
6
7
8
9
10
11
12function Fun() {
}
var fun = new Fun()
Fun.prototype.test = function () {
}
console.log(fun.__proto__ === Fun.prototype) // true
fun.test()
原型链
- 访问一个对象的属性时,
- 先在自身中查找, 找到返回
- 如果自身没有, 再沿着
__proto__
这条链向上查找, 找到返回 - 如果最终没找到, 返回 undefined
- 别名 —— 隐式原型链
- 作用 —— 查找对象的属性(方法)
原型的继承
- 构造函数的实例对象自动拥有构造函数的原型对象的属性(方法)
利用的就是原型链
注意
函数的显式原型指向的对象默认是空 Object 实例对象(但 Object 不满足)
1
console.log(Object.prototype instanceof Object) // false
所有的函数都是 Function 的实例 (包含 Function)
1
console.log(Function.__proto__ === Function.prototype) // true
Object 的原型对象是原型链的尽头
1
console.log(Object.prototype.__proto__) // null
原型链属性问题
- 读取对象的属性值时, 会自动到原型链中查找
- 设置对象的属性值时, 不会查找原型链, 如果当前对象自身没有此属性, 直接添加此属性并设置其值
- 方法一般定义在原型上, 属性一般通过构造函数定义在对象自身上
1
2
3
4
5
6
7
8
9
10function Fn () {
}
Fn.prototype.a = 'xx'
var fn1 = new Fn()
var fn2 = new Fn()
fn2.a = 'yy' // 设置对象的属性值, 不会查找原型链
console.log(fn1.a, fn2.a) // 'xx' 'yy'
执行上下文与执行上下文栈
变量提升和函数提升
变量提升
- 使用 var 声明的 变量, 在定义语句之前就可以访问到
- 值 —— undefined
1
2
3
4if(!(b in window)) {
var b = 1 // 声明提前
}
console.log(b) // undefined
函数声明提升
- **通过 function 声明的 **函数, 在声明之前就可以直接调用
- 函数表达式方式不会提升
变量提升和函数提升是如何产生的?
- 执行上下文预处理
⚠️先执行变量提升
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function a () {}
var a
console.log(typeof a) // 'function'
var c = 1
function c(c) {
console.log(c)
var c = 3
}
c(2) // c is not a function
// 相当于
var c
function c(c) {...}
c = 1
c(2)
执行上下文
全局执行上下文
- 在
执行全局代码前
, 将 window 确定为全局执行上下文 - 对全局数据进行预处理
- var 定义的全局变量, 添加为 window 的属性(值为 undefined)
- function 声明的函数, 添加为 window 的方法
- this 设置为 window
- 开始执行全局代码
- 在
函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
- 对局部数据进行预处理
- var 定义的局部变量, 添加为执行上下文的属性(值为 undefined)
- function 声明的函数, 添加为执行上下文的方法
- 形参被赋值为实参值, 添加为执行上下文的属性
- arguments 赋值为实参列表, 添加为执行上下文的属性
- this 赋值为
调用函数的对象
- 开始执行函数体
执行上下文栈
- 在全局代码执行前, JS 引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
- 在函数执行上下文创建后, 将其添加到栈中
- 在当前函数执行完毕后, 将栈顶的对象移除(出栈)
- 当所有的代码执行完毕后, 栈中只剩下 window
作用域与作用域链
作用域
- 就是一块“地盘”, 一个代码所在的区域
- 它是静态的(相对于上下文对象), 在编写代码的时候就确定了
- 分类
- 全局作用域
- 函数作用域
- 没有块作用域 (ES6 有了)
- 作用
- 隔离变量, 不同作用域下同名变量不会有冲突
作用域与执行上下文
- 区别1
- 全局作用域之外, 每个函数都会创建自己的作用域, 作用域在函数定义的时候就已经确定了, 而不是在函数调用的时候
- 全局执行上下文是在全局作用域确定之后, js 代码执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
- 区别2
- 作用域时静态的, 只要函数定义好了就一直存在, 且不会再变化
- 函数执行上下文是动态的, 调用函数时创建, 函数结束调用就会自动释放
- 联系
- 执行上下文对象是从属于所在的作用域
作用域链
- 多个上下级函数的作用域形成的链, 它的方向是从下向上 (从内向外) 的
查找变量
时就是沿着作用域链来查找的 (查找对象的属性是沿着原型链来查找的)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21var x = 10
function fn() {
console.log(x)
}
function show(f) {
var x = 20
f()
}
show(fn) // 10 作用域在函数声明时候就确定了
var fn = function () {
console.log(fn)
}
fn() // function () { console.log(fn) }
var obj = {
fn2: function () {
console.log(fn2)
}
}
obj.fn2() // fn2 is not defined
闭包closure
如何产生闭包?
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
闭包到底是什么?
- 理解 1: 闭包是嵌套的内部函数
- 理解 2: 包含被引用变量(或函数)的对象 - closure
- ⚠️ 闭包存在于嵌套的内部函数中
产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量 / 函数)
常见的闭包
- 将函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 例 1
function fn1() {
var a = 2
function fn2 () { // 执行内部函数定义时,闭包产生
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3 在外部操作内部变量 a
f() // 4
// 例 2
function showDelay(msg, time) {
setTimeout(function () {
alert(msg)
}, time)
}
showDelay('hello', 2000)
闭包的作用
- 使用函数内部的变量在函数执行完毕后, 仍然存活在内存中 (延长了
局部变量的生命周期
) - 让函数外部可以操作(读 / 写)到函数内部的数据(变量 / 函数)
- 问题?
- 函数执行完后, 函数内部声明的局部变量是否还存在?
- 一般不存在, 存在于闭包中的变量有可能存在 (见 例 1)
- 在函数外部能直接访问函数内部的局部变量吗?
- 不能, 但是通过闭包可以让外部操作它 (见 例 1)
- 函数执行完后, 函数内部声明的局部变量是否还存在?
闭包的生命周期
产生
- 在
嵌套的内部函数定义时
就产生了(不是在被调用时) - 外部函数调用几次就产生几个闭包
死亡
在嵌套的内部函数成为垃圾对象时 (不再有变量去访问它时)
1
2
3
4
5
6
7
8
9
10
11
12
13function fn1() {
var a = 2
function fn2 () { // 执行内部函数定义时,闭包产生
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3 在外部操作内部变量 a
f() // 4
f = null // 不再有变量可以直接访问到 fn2 了, 闭包死亡(fn2 在被 js 回收前还存在内存中)
闭包的应用
定义 JS 模块
- 具有特定功能的
JS 文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包含 n 个属性(方法)的对象或函数
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14// myModule.js
(function myModule() {
var msg = 'hEllo'
function doSomething() {
console.log(msg.toUpperCase())
}
function doOtherthing() {
console.log(msg.toLowerCase())
}
window.myModule = {
doSomething: doSomething,
doOtherthing: doOtherthing
}
})()
1 | // html 文件 |
闭包的缺点
缺点
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间变长
- 容易造成内存泄漏
解决
- 能不用闭包就不用
- 及时释放 —— 让引用的函数成为垃圾对象 (回收闭包)
内存溢出和内存泄漏
内存溢出
- 一种程序运行出现的错误
- 当
程序运行需要的内存超过了剩余的内存
时, 就会抛出内存溢出的错误
内存泄漏
- 占用的内存没有及时释放
- 内存泄漏积累多了就容易导致内存溢出
- 常见的内存泄漏
- 意外的全局变量
- 没有及时清理的计时器或回调函数
- 闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 意外的全局变量
function () {
a = new Array(10000000)
console.log(a)
}
// 没有及时清理的计时器
setInterval(function () {
console.log('aaaa')
}, 1000)
// 闭包
function fn1 () {
var a = 1
function fn2 () {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f()
面向对象高级
对象创建方式
方式一 —— Object 构造函数模式
- 套路: 先创建空 Object 对象, 再动态添加属性 / 方法
- 适用场景: 起始不确定对象内部数据
- 问题: 语句太多
1
2
3
4
5
6
7
8
9
10
11
12
13var p1 = new Object()
p1.name = '章三'
p1.age = 18
p1.setName = function (name) {
this.name = name
}
var p2 = new Object()
p2.name = '里斯'
p2.age = 22
p2.setName = function (name) {
this.name = name
}
方式二 —— 字面量模式 (常用)
- 套路: 使用 {} 创建对象, 同时指定内部属性
- 适用场景: 起始时对象内部数据是确定的
- 问题: 如果创建多个对象, 有重复代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var p1 = {
name: '章三',
age: 18,
setName: function (name) {
this.name = name
}
}
var p2 = {
name: '里斯',
age: 22,
setName: function (name) {
this.name = name
}
}
方式三 —— 工厂模式 (返回一个对象的函数)
- 套路: 通过工厂函数动态创建对象并返回
- 适用场景: 需要创建多个对象
- 问题:对象没有一个具体的类型, 都是 Object 类型
1
2
3
4
5
6
7
8
9
10
11
12function createPerson(name, age) {
var obj = {
name: name,
age: age,
setName: function (name) {
this.name = name
}
}
return obj // 返回一个对象的函数 —— 工厂函数
}
var p1 = createPerson('Tom', 16) // p1、p2 都是 Object 类型
var p2 = createPerson('Bob', 18)
方式四 —— 自定义构造函数模式
- 套路: 自定义构造函数, 通过 new 创建对象
- 适用场景: 需要创建多个类型确定的对象
- 问题: 每个对象都有相同的数据(
主要指方法
), 浪费内存1
2
3
4
5
6
7
8
9function Person(name, age) {
this.name = name
this.age = age
this.setName = function (name) {
this.name = name
}
}
var p1 = new Person('Tom', 16)
var p2 = new Person('Bob', 18)
方式五 —— 自定义构造函数 + 原型模式(常用)
- 套路: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上
- 适用场景: 需要创建多个类型确定的对象
1
2
3
4
5
6
7
8
9function Person(name, age) { // 在构造函数中,只初始化一般属性
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 17)
var p2 = new Person('Bob', 20)
原型链的继承
- 原理
- 子类型的原型为父类型的一个实例
1
2Son.prototype = new Father() // 为了能看到父类型的方法
Son.prototype.constructor = Son // 修正 constructor 属性
- 子类型的原型为父类型的一个实例
线程机制与事件机制
进程与线程
- 进程
- 程序的一次运行, 它占用独有的一块内存空间
- 可以通过 windows 任务管理器查看
- 线程
- 是进程内一个独立的执行单元
- 是程序执行的一个完整流程
- 是 CPU 的最小调度单元
- 多线程优缺点
- 优点
- 能有效提升 CPU 的利用率
- 缺点
- 创建多线程开销
- 线程间切换开销
- 死锁与状态同步问题
- 优点
- JS 是
单线程
运行的- js 引擎执行代码的基本流程
- 先执行初始化代码
- 设置定时器
- 绑定监听
- 发送 ajax 请求
- 后面某个时刻才会执行回调代码
- 先执行初始化代码
- H5 中的 Web Workers 可以多线程运行
- js 引擎执行代码的基本流程
- 为什么 js 是单线程的?
- 与它的用途有关
- 作为浏览器脚本语言, js 的主要用途是与用户互动, 以及操作 DOM
- 这决定了它只能是单线程, 否则会带来很复杂的问题 (同时操作会有冲突)
浏览器内核
- 支撑浏览器运行的最核心的程序
- 内核由很多模块组成
- 主线程
- js 引擎模块 —— 负责 js 程序的编译和运行
- html/css 文档解析模块 —— 负责页面文本的解析
- DOM/CSS 模块 —— 负责 dom/css 在内存中的相关处理
- 布局与渲染模块 —— 负责页面的布局和效果显示
- 分线程
- 定时器模块 —— 管理定时器
- DOM 事件模块 —— 管理事件
- 网络请求模块 —— 负责 ajax 请求
- 主线程
- Chrome / Safari —— webkit
- Firefox —— Gecko
- IE —— Trident
定时器
- 定时器是在主线程执行的 (js 是单线程运行的)
- 定时器是如何实现的?
- 事件循环模型
事件循环模型
- 代码分类
- 初始化执行代码 —— 包含 dom 监听、设置定时器、发送 ajax 请求
- 回调执行代码 —— 处理回调逻辑
- js 引擎执行代码的基本流程
- 初始化代码 >>> 回调代码
- 模型的两个组成部分
- 事件管理模块
- 回调队列
- 模型的运转流程
- 执行初始化代码时, 将事件回调函数交给对应的模块管理
- 当事件发生时, 管理模块会将回调函数及其数据添加到回调队列中
- 只有当初始化代码执行完后, 才会遍历读取回调队列中的回调函数执行
H5 Web Workers (多线程)
⚠️ 用的少, 但面试会问到
H5 规范提供了 js 多线程的实现, 叫 web workers
- Web Workers 是 html5 提供的一个 js 多线程解决方案
- 我们可以将一些大计算量的代码交由 Web Workers 运行而不冻结用户界面
但是子线程完全受主线程控制, 且不得操作 DOM - 所以, 这个新标准并没有改变 js 单线程的本质
相关 API
- Worker
- 构造函数, 加载分线程执行的 js 文件
- Worker.prototype.onmessage
- 用于接收另一个线程的回调函数
- Worker.prototype.postMessage
- 向另一个线程发送消息
- Worker
不足
- worker 内代码不能操作 DOM
- 看不到 window, 分线程内的全局对象不是 window, 不能调用 window 的方法, 所以不能更新界面
- 不能跨域加载 JS
- 不是每个浏览器都支持
- 慢
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// 主线程代码
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)
}
// worker.js
function fibonacci (n) {
return n <=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)
}
console.log (this) // 不是 window
var onmessage = function (event) {
console.log('分线程接收到主线程数据:' + event.data)
var number = event.data // 通过 event.data 获取发送过来的数据
postMessage(fibonacci(number)) // 将获取到的数据发给主线程
}
- worker 内代码不能操作 DOM