JavaScript 高级笔记
- 1. 基础总结深入
- 2. 函数
- 3. 函数高级
- 4. 面向对象
- 5. 面向对象高级
- 6. 线程机制与事件机制
- 7. 深入了解 JavaScript 内存管理
- 8. this 九重关
1. 基础总结深入
1.1 数据类型
1.1.1 数据类型分类
- 基本(
值
)类型String:任意字符串
Number:任意的数字
boolean:true/false
undefined:undefined
null:null
- 对象(
引用
)类型Object:任意对象
Function:一种特别的对象(可以执行)
Array:一种特别的对象(数值下标, 内部数据是有序的)
1.1.2 数据类型判断
typeof
- 可以判断:
undefined/ 数值 / 字符串 / 布尔值 / function
- 不能判断:
null与object object与array
- 可以判断:
instanceof
判断对象的具体类型
===
- 可以判断:
undefined, null
- 可以判断:
//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'
-------------------------------------------------------------------------------------
//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()())
1.1.3 数据类型相关问题
undefined 与 nul l的区别?
- undefined 代表定义未赋值
- null 定义并赋值了, 只是值为 null
// 1. undefined与null的区别?
var a
console.log(a) // undefined
a = null
console.log(a) // null
什么时候给变量赋值为 null 呢?
- 初始赋值, 表明将要赋值为对象
- 结束前, 让对象成为垃圾对象(被垃圾回收器回收)
//起始
var b = null // 初始赋值为null, 表明将要赋值为对象
//确定对象就赋值
b = ['atguigu', 12]
//最后
b = null // 让b指向的对象成为垃圾对象(被垃圾回收器回收)
// b = 2
严格区别变量类型与数据类型?
- 数据的类型
- 基本类型
- 对象类型
- 变量的类型(
变量内存值的类型
)- 基本类型: 保存就是基本类型的数据
引用类型: 保存的是地址值
var c = function () {
}
console.log(typeof c) // 'function'
1.2 数据、变量与内存
1.2.1 什么是数据
存储在内存中代表特定信息的'东东', 本质上是二进制0101...
- 数据的特点:
可传递, 可运算
- 一切皆数据,函数也是数据
- 内存中所有操作的目标: 数据
- 算术运算
- 逻辑运算
- 赋值
- 运行函数(调用函数传参)
1.2.2 什么是内存
内存条通电后产生的可储存数据的空间(临时的)
内存产生和死亡
内存条(电路板)==>通电==>产生内存空间==>存储数据==>处理数据==>断电==>内存空间和数据都消失
内存的空间是临时的,而硬盘的空间是持久的
分配内存
声明变量和函数或创建对象时,JS 引擎会自动为此分配一定大小的内存来存放对应的数据
释放内存
清空内存中的数据,标识内存可以再分配使用(内存不释放就不能复用)
自动释放
- 栈空间的局部变量
垃圾回调器回调
堆空间的垃圾对象
- 一块小内存的2个数据
内部存储的数据
地址值
内存分类
栈: 全局变量/局部变量
堆: 对象
1.2.3 什么是变量
可变化的量, 由变量名和变量值组成
- 每个变量都对应的一块小内存,
变量名用来查找对应的内存, 变量值就是内存中保存的数据
1.2.4 内存,数据, 变量三者之间的关系
内存是一个容器, 用来存储程序运行需要操作的数据
变量是内存的标识, 通过变量找到对应的内存,进而操作(读/写)内存中的数据
1.2.5 相关问题
关于赋值和内存的问题
问题: var a = xxx, a内存中到底保存的是什么?
* xxx是基本数据, 保存的就是这个数据
* xxx是对象, 保存的是对象的地址值
* xxx是一个变量, 保存的xxx的内存内容(可能是基本数据, 也可能是地址值)
关于引用变量赋值问题
关于引用变量赋值问题
* 2个引用变量指向同一个对象, 通过一个变量修改对象内部数据, 另一个变量看到的是修改之后的数据
* 2个引用变量指向同一个对象, 让其中一个引用变量指向另一个对象, 另一引用变量依然指向前一个对象
var obj1 = {name: 'Tom'}
var obj2 = obj1
obj2.age = 12
console.log(obj1.age) // 12
function fn (obj) {
obj.name = 'A'
}
fn(obj1)
console.log(obj2.name) //A
var a = {age: 12}
var b = a
a = {name: 'BOB', age: 13}
b.age = 14
console.log(b.age, a.name, a.age) // 14 Bob 13
function fn2 (obj) {
obj = {age: 15}
}
fn2(a)
console.log(a.age) // 13
关于数据传递问题
问题: 在js调用函数时传递变量参数时, 是值传递还是引用传递
* 理解1: 都是值(基本/地址值)传递
* 理解2: 可能是值传递, 也可能是引用传递(地址值)
var a = 3
function fn (a) {
a = a + 1
}
fn(a)
console.log(a) // 3
function fn2 (obj) {
console.log(obj.name)
}
var obj = {name: 'Tom'}
fn2(obj) // Tom
JS引擎如何管理内存
问题: JS引擎如何管理内存?
1. 内存生命周期
* 分配小内存空间, 得到它的使用权
* 存储数据, 可以反复进行操作
* 释放小内存空间
2. 释放内存
* 局部变量: 函数执行完自动释放
* 对象: 成为垃圾对象==>垃圾回收器回收
var a = 3
var obj = {}
obj = undefined
function fn () {
var b = {}
}
fn() // b是自动释放, b所指向的对象是在后面的某个时刻由垃圾回收器回收
1.3 对象
1.3.1 什么是对象
- 多个数据的
封装体
- 用来
保存多个数据
的容器
- 一个对象代表现实中的一个
事物
1.3.2 为什么要用对象
统一管理多个数据
1.3.3 对象的组成
属性
- 代表现实事物的状态数据
- 由属性名(字符串)和属性值(任意)组成
方法
- 代表现实事物的行为数据
- 一种
特别的属性(属性值是函数)
1.3.4 如何访问对象内部数据
.属性名
: 编码简单, 有时不能用['属性名']
: 编码麻烦, 能通用
var p = {
name: 'Tom',
age: 12,
setName: function (name) {
this.name = name
},
setAge: function (age) {
this.age = age
}
}
p.setName('Bob')
p['setAge'](23)
console.log(p.name, p['age'])
什么时候必须使用['属性名']的方式
属性名包含特殊字符: - 空格
属性名不确定
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])
1.4 对象方法
函数
也可以称为对象的属性
如果一个函数作为一个对象的属性保存
- 那么称这个函数是这个对象的
方法
调用函数
就说调用对象的方法(method)
- 只是
名称上的区别,没有其他区别
// 创建一个对象
var obj = new Object();
// 向对象中添加属性
obj.name = 'zs';
obj.age = 18;
// 对象的属性值可以是任何的数据类型,也可以是个函数
obj.sayName = function(){
console.log(obj.name);
};
function fun(){
console.log(obj.name);
}
// console.log(obj.sayName);
// 调方法
obj.sayName();
// 调函数
fun();
1.5 枚举对象中的属性
- 枚举对象中的属性
- 使用
for...in
语句
- 使用
语法
for(var 变量 in 对象){}
for...in
语句对象中有几个属性,循环体就会执行几次
每次执行时,会将对象中的一个属性的名字赋值给变量
for(var n in obj){
console.log(n) // 属性名
console.log(obj[n]) // 属性值
}
1.6 作用域
- 任何程序设计语言都有作用域的概念,简单的说,
作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期
function test(o) {
var i = 0;
if (typeof o == "object") {
var j = 0
for(var k = 0; k < 10; k++) {
console.log(k)
}
console.log(j)
}
}
test({})
var scope = "global"
function f() {
console.log(scope);
var scope = "local"
console.log(scope);
}
f()
1.6.1 全局作用域
- 在代码中任何地方都能访问到的对象拥有全局作用域
- 以下三种情形拥有全局作用域:
- 最外层函数本身,以及在最外层函数外面定义的变量
- 所有未定义直接赋值的变量
- 所有window对象的属性
- 全局作用域
在页面打开时创建,在页面关闭时销毁
- 在全局作用域中
创建的变量都会作为 window 对象的属性保存
创建的函数都会作为 window 对象的方法保存
- 将函数定义在全局作用域,
污染了全局作用域的命名空间
- 而且定义在全局作用域中也很
不安全
- 而且定义在全局作用域中也很
// 三种拥有全局变量的情形
// 1. 最外层函数本身,以及在最外层函数外面定义的变量
var global = "global";
function checkscope() {
var local = "local";
return global;
}
console.log(global); // "global"
console.log(checkscope()); // "global"
console.log(local); // error: local is not defined
// 2. 所有未定义直接赋值的变量
function checkscope() {
var local = "local";
global = "global";
}
console.log(global); // "global"
console.log(local); // error: local is not defined
// 3. 所有window对象的属性
window.aaa = "aaa";
window.checkscope = function() {
return aaa;
}
console.log(aaa); // "aaa"
console.log(checkscope()); // "aaa"
console.log(name);
console.log(location);
// 扩展1: 在全局作用域中声明的所有变量和函数,都会成为window对象的属性
var color = "red"; // window.color = "red"
function sayColor() { // window.sayColor = function()
return window.color;
}
console.log(window.sayColor()); // "red"
console.log(window)
// 扩展2: var color 和 window.color 有区别,
// 通过 var 语句声明的变量不能被删除,通过 function 语句定义的函数也是不能被删除的
var color1 = "red"
function sayColor1() {
return window.color1;
}
console.log(delete color1);
console.log(delete sayColor1);
console.log(delete window.color1d);
console.log(delete window.sayColor1);
console.log(color1);
console.log(sayColor1);
window.color2 = "green";
window.sayColor2 = function() {
return window.color2;
}
console.log(delete color2);
console.log(delete sayColor2);
console.log(delete window.color2);
console.log(delete window.sayColor2);
console.log(color2);
console.log(sayColor2);
1.6.2 局部作用域
- 和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到
- 最常见的是在函数体内定义的变量,只能在函数体内使用
// 1. 局部作用域
function checkscope() {
var local = "local"
return local;
}
console.log(checkscope()); // "local"
console.log(local); // error: local is not defined
1.6.3 全局作用域和局部作用域的关系
- 在函数体内,局部变量的优先级高于同名的全局变量
- 如果在函数内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就被局部变量所遮盖
// 全局作用域和局部作用域的关系
var scope = "golbal";
function checkscope() {
var scope = "local"
return scope;
}
console.log(checkscope());
1.6.4 声明提前
- JavaScript 函数里声明的所有变量(但不涉及赋值)都被提前至函数体的顶部
- 代码优化
- 将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处
- 这种做法使得他们的源代码非常清晰的反映了真实的变量作用域
console.log(a); // undefined
var a = 123;
函数声明提前
使用函数声明形式创建的函数 function 函数()
它会在所有的代码执行之前就被创建
使用函数表达式创建的函数,不会被声明提前,所以不能在声明前调用
fun()
fun2() // undefined is not a function
// 函数声明,会被提前创建
function fun(){
console.log(1212)
}
// 函数表达式,不会被提前创建
var fun2 = function(){
console.log(2121)
}
1.6.5 函数作用域
- JavaScript 中并没有块级作用域,取而代之的使用了函数作用域,变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的
// 1. 函数作用域
function test(o){
var i = 0;
if(typeof o == "object"){
var j = 0;
for(var k = 0; k < 10; k++){
console.log(k); // 0-9
}
console.log(k); // 10
}
console.log(j); // 0
}
test({});
调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁
每调用一次函数就会创建一个新的函数作用域
,他们之间时相互独立
的- 在
函数作用域
中可以访问到全局作用域的变量
- 在
全局作用域
中无法访问到函数作用域的变量
- 当
在函数作用域操作一个变量时,它会先在自身作用域中寻找
- 如果
有就直接使用
- 如果
没有则向上一级作用域中寻找,直到找到全局作用域
- 如果
全局作用域中依然没有找到,则会报错 ReferenceError
- 如果
在函数中要访问全局变量可以使用 window 对象
// 创建一个变量
var a = 10;
function fun(){
var a = 12
var b = 20;
console.log(a) // 12
}
fun()
console.log(b) // b is undefined
console.log(a) // 10
- 在
函数作用域也有声明提前的特性
使用 var 关键字声明的变量,会在函数中所有的代码执行之前被声明
函数声明也会在函数中所有的代码执行之前执行
function fun3(){
fun4()
// console.log(a) // undefined
var a = 32
function fun4(){
alert(12)
}
}
fun3()
- 在函数中,
不使用 var 声明的变量都会成为全局变量
var c = 33
function fun5(){
// console.log(c) // undefined
// var c = 10
console.log(c) // 33
// d 没有使用 var 关键字,则会设置为全局变量
d = 100
}
fun5()
// 在全局输出c
console.log(d) // 100
定义形参就相当于在函数作用域中声明了变量
var e = 23
function fun6(e){
alert(e) // undefined
}
fun6()
1.6.6 作用域链
- 基本概念
- 在JavaScript中一切都是对象,函数也是对象
- 实际上,函数对象和其他对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性
- 其中一个内部属性是scope,该属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问
- 作用域链的前端,始终都是当前执行的代码所在环境的变量对象,该对象包含了函数的所有局部变量、参数以及this
- 下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境,就这样一直延续到全局作用域,全局作用域始终都是作用域链中的最后一个对象
// 作用域链
var aaa = "aaa"
var fun1 = function(args1) {
debugger;
var bbb = "bbb"
var func2 = function(args2){
debugger;
var ccc = "ccc"
function func3(args3) {
debugger;
var ddd = "ddd"
console.log(aaa);
console.log(bbb);
console.log(ccc);
console.log(ddd);
}
func3(3)
}
func2(2)
}
func1(1)
- 核心
- 在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据
- 该过程从作用域链头部,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义
- 函数执行过程中,每个标识符都要经历这样的搜索过程
- 开发问题
- 问题一
- 从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢
- 解决方法
- 如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用
- 问题二
- 使用with关键字,会改变作用域链,从而产生性能问题
- 解决方法
- 尽量避免使用with关键字
- 问题一
1.7 使用工厂方法创建对象
- 通过该方法可以
大批量的创建对象
- 使用工厂方法创建的对象,
使用的构造函数都是 Object
- 所以
创建的对象都是 Object 这个类型
- 就导致我们
无法区分出多种不同类型的对象
- 所以
// 创建对象
var obj = {
name: 'zs',
age: 18,
sayName: function(){
alert(this.name)
}
}
function createPerson(name, age, gender){
// 创建一个新的对象
var obj = new Object()
// 向对象中添加属性
obj.name = name;
obj.age = age;
obj.gender = gender;
obj.sayName: function(){
alert(this.name);
}
// 将新的对象返回
return obj;
}
var obj2 = createPerson('zx',21,'男')
console.log(obj2)
1.8 原型对象
原型 prototype
- 我们所创建的每一个函数,解析器都会向函数中添加一个属性
prototype
这个属性对应着一个对象
,这个对象就是我们所谓的原型对象
- 我们所创建的每一个函数,解析器都会向函数中添加一个属性
- 如果函数作为
普通函数调用 prototype 没有任何作用
- 当函数以
构造函数的形式调用
时- 它所创建的对象中都会有一个
隐含的属性
指向该构造函数的原型对象
- 可以
通过 __proto__ 来访问该属性
- 它所创建的对象中都会有一个
原型对象
就相当于一个公共的区域
所有同一个类的实例都可以访问到这个原型对象
- 可以将对象中
共有的内容,统一设置到原型对象中
- 当我们访问对象的一个属性或方法时,它会
先在对象自身中寻找
- 如果
有,则直接使用
- 如果
没有,则会去原型对象中寻找,如果找到则直接使用
- 如果
- 以后我们
创建构造函数
时,可以将这些对象共有的属性和方法
统一添加到构造函数的原型对象中
- 这样
不用分别为每一个对象添加
- 也
不会影响到全局作用域
- 就可以
使每个对象都具有这些属性和方法
function MyClass(){
}
// 向 MyClass 的原型中添加属性 a
MyClass.prototype.a = 123;
// 向 MyClass 的原型中添加一个方法
MyClass.prototype.sayHello = function(){
alert('hello');
};
var mc = new MyClass();
var mc2 = new MyClass();
// console.log(MyClass.prototype); // [object Object]
// console.log(mc.__proto__); // [object Object]
// console.log(mc2.__proto__ == MyClass.prototype); // true
console.log(mc.a) // 123
mc.sayHello();
- 使用
in
检查对象中是否含有某个属性
时- 如果
对象中没有,但是原型中有,也会返回 true
- 如果
- 可以使用对象的
hasOwnProperty()
来检查对象自身中是否含有该属性
- 使用该方法
只有当对象自身中含有属性时,才会返回 true
- 使用该方法
console.log("name" in mc)
console.log(mc.hasOwnProperty("name"))
原型对象也是对象
,所以它也有原型
- 当我们使用一个对象的属性或方法时,会
先在自身中寻找
自身中如果有,则直接使用
如果没有,则去原型对象中寻找,如果原型对象中有,则使用
如果没有,则去原型对象的原型对象中寻找,直到找到 Object 对象的原型
- 当我们使用一个对象的属性或方法时,会
Object 对象的原型没有原型对象
如果在 Object 中依然没有找到,则返回 undefined
console.log(mc.__proto__.__proto__.hasOwnProperty("hasOwnProperty"))
1.9 垃圾回收
垃圾回收(GC)
- 程序运行过程中也会产生垃圾
- 这些
垃圾积攒过多以后,会导致程序运行的速度过慢
- 所以
需要一个垃圾回收的机制,来处理程序运行过程中产生的垃圾
当一个对象没有任何的变量或属性对它进行引用
此时我们将永远无法操作该对象
- 此时这种对象就是一个
垃圾
,这种对象过多占用大量的内存空间,导致程序运行变慢
- 所以这种垃圾必须
清理
- 在 JS 中拥有
自动的垃圾回收机制
,会自动将这些垃圾对象从内存中销毁
- 我们
不需要也不能进行垃圾回收的操作
- 我们
- 我们需要做的只是要
将不再使用的对象设置为 null 即可
var obj = new Object();
obj = null
2. 函数
2.1 什么是函数
实现特定功能的 n 条语句的封装体
只有函数是可以执行的, 其它类型的数据不能执行
- 使用
typeof
检查一个函数对象时,会返回function
// 创建一个函数对象
// 可以将要封装的代码以字符串的形式传递给构造函数
var fun = new Function("console.log('Hello 这是我的第一个函数');")
// 封装到函数中的代码不会立即执行
// 函数中的代码会在函数调用的时候执行
// 调用函数 语法:函数对象()
// 当调用函数时,函数中封装的代码会按照顺序执行
fun();
/*
使用函数声明来创建一个函数
语法:
function 函数名([形参1,形参2...形参N]){
语句
}
*/
function fun2(){
console.log("第二个函数")
alert("哈哈哈")
document.write("123")
}
// console.log(fun2)
// 调用 fun2
fun2()
/*
使用函数表达式来创建一个函数
语法:
var 函数名 = function([形参1, 形参2...形参N]){
语法...
}
*/
var fun3 = function(){
console.log("我是匿名函数中封装的代码")
};
fun3()
2.2 为什么要用函数
提高代码复用
便于阅读交流
/*
编写程序实现以下功能需求:
1. 根据年龄输出对应的信息
2. 如果小于18, 输出: 未成年, 再等等!
3. 如果大于60, 输出: 算了吧!
4. 其它, 输出: 刚好!
*/
function showInfo (age) {
if(age<18) {
console.log('未成年, 再等等!')
} else if(age>60) {
console.log('算了吧!')
} else {
console.log('刚好!')
}
}
showInfo(17)
showInfo(20)
showInfo(65)
2.3 如何定义函数
函数声明
函数表达式
var fun = new Function(){...} // 构造函数
function fn1 () { // 函数声明
console.log('fn1()')
}
var fn2 = function () { // 函数表达式
console.log('fn2()')
}
fn1()
fn2()
2.4 如何调用(执行)函数
test(): 直接调用
obj.test(): 通过对象调用
new test(): new调用
test.call/apply(obj): 临时让 test 成为 obj 的方法进行调用
var obj = {}
function test2 () {
this.xxx = 'atguigu'
}
// obj.test2() 不能直接, 根本就没有
test2.call(obj) // obj.test2() // 可以让一个函数成为指定任意对象的方法进行调用
console.log(obj.xxx)
2.5 函数的参数
形参
- 可以
在函数的()中来指定一个或多个形参(形式参数)
多个形参之间使用,隔开
,声明形参就相当于在函数内部声明了对应的变量
- 但是
并不赋值
- 可以
function sum(a,b){
console.log(a+b);
}
实参
- 在调用函数时,可以
在()中指定实参(实际参数)
实参将会赋值给函数中对应的形参
- 在调用函数时,可以
sum(1,2)
调用函数时解析器不会检查实参的类型
- 所以要注意,
是否有可能会接收到非法的参数
,如果有可能则需要对参数进行类型的检查
函数的实参可以是任意的数据类型
sum(123,'hello')
sum(true, false)
- 调用函数时,
解析器也不会检查实参的数量
多余的实参不会被赋值
- 如果
实参的数量少于形参的数量
,则没有对应实参的形参将是 undefined
sum(123,345,'hello',true,null)
sum(123)
2.6 函数的返回值
- 可以使用
return
来设置函数的返回值
语法
return 值
return 后的值
将会作为函数的执行结果
返回,可以定义一个变量,来接收该结果- 在函数中
return 后的语句都不会执行
- 如果
return 语句后不跟任何值就相当于返回一个 undefined
- 如果
函数中不写 return,则也会返回 undefined
return 后可以跟任意类型的数据类型,也可以是个对象,也可以是个函数
// 创建一个函数,用来计算三个数的和
function sum(a,b,c){
var d = a + b + c;
return d;
// alert('123')
// return;
// return undefined;
}
// 调用函数
// 变量 result 的值就是函数的执行结果
// 函数返回什么 result 的值就是什么
var result = sum(4,7,8)
console.log(result)
返回值的类型
for(var i=0; i<5; i++){
if(i == 2){
// 使用 break 可以退出当前的循环
// break;
// continue 用于跳出当次循环
// continue;
// 使用 return 可以结束整个函数
return;
}
}
2.7 IIFE
- 全称:
立即调用函数表达式
(Immediately-Invoked Function Expression
)
作用
隐藏实现
不会污染外部(全局)命名空间
用它来编码js模块
(function () { //匿名函数自调用
var a = 3
console.log(a + 3) // 6
})()
var a = 4
console.log(a) // 4
;(function () {
var a = 1
function test () {
console.log(++a) // 2
}
window.$ = function () { // 向外暴露一个全局函数
return {
test: test
}
}
})()
$().test() // 1. $是一个函数 2. $执行后返回的是一个对象
2.8 函数中的 this
2.8.1 this 是什么
- 解析器在调用函数
每次都会向函数内部传递一个隐含的参数
- 这个
隐含的参数就是 this,this 指向的是一个对象
- 这个对象称为
函数执行的上下文对象
- 根据
函数的调用方式的不同
,this 会指向不同的对象
- 以
函数
的形式调用时,this 永远都是 window
- 以
方法
的形式调用时,this 就是调用方法的那个对象
- 以
- 这个
任何函数本质上都是通过某个对象来调用的,如果没有直接指定就是 window
所有函数内部都有一个变量 this
它的值是调用函数的当前对象
2.8.2 如何确定 this 的值
- 当以
函数
的形式调用时,this 是 window
- 当以
方法
的形式调用时,谁调用方法 this 就是谁
在构造函数中,this 指向新建对象
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(); //this是谁? window
2.9 JS 中的分号问题
- js 一条语句的后面
可以不加分号
- 是否加分号是
编码风格问题
, 没有应该不应该,只有你自己喜欢不喜欢 在下面2种情况下不加分号会有问题
小括号开头的前一条语句
中方括号开头的前一条语句
- 解决办法:
在行首加分号
- 强有力的例子: vue.js库
var a = 3
;(function () {
})()
/*
错误理解
var a = 3(function () {
})();
*/
var b = 4
;[1, 3].forEach(function () {
})
/*
错误理解
var b = 4[3].forEach(function () {
})
*/
2.10 构造函数
- 构造函数就是一个普通的函数,
创建方式和普通函数没有区别
- 不同的是构造函数习惯上
首字母大写
- 不同的是构造函数习惯上
- 构造函数和普通函数的区别就是
调用方式的不同
普通函数是直接调用
构造函数需要使用 new 关键字来调用
function Person(){}
var per = new Person()
console.log(per)
- 构造函数的
执行流程
立刻创建一个新的对象
将新建的对象设置为函数中 this
在构造函数中可以使用 this 来引用新建的对象
逐行执行函数中的代码
将新建的对象作为返回值返回
function Person(name, age){
this.name = name;
this.age = age;
}
var per = new Person('zx',18)
console.log(per)
- 使用
同一个构造函数创建的对象
,称为一类对象,也将一个构造函数称为一个类
- 我们将
通过一个构造函数创建的对象,称为是该类的实例
- 使用
instanceof
可以检查一个对象是否是一个类的实例
- 语法:
对象 instanceof 构造函数
- 如果
是,则返回 true
- 如果
不是,则返回 false
- 语法:
所有的对象都是 Object 的后代
- 所以
任何对象和 Object 做 instanceof 检查时都会返回 true
- 所以
console.log(per instanceof Person)
2.11 回调函数
-
什么函数才是回调函数?
- 你
定义
的函数 - 你
没有调用
这个函数 - 但
最终这个函数执行
了(在某个时刻或某个条件下
)
- 你
-
常见的回调函数
dom 事件
回调函数 ==>发生事件的 dom 元素定时器
回调函数 ===> windowajax 请求
回调函数生命周期
回调函数
document.getElementById('btn').onclick = function () { // dom事件回调函数
alert(this.innerHTML)
}
//定时器
// 超时定时器
// 循环定时器
setTimeout(function () { // 定时器回调函数
alert('到点了'+this)
}, 2000)
3. 函数高级
3.1 原型与原型链
3.1.1 原型 prototype
函数的 prototype 属性(图)
- 每个函数都有一个
prototype
属性, 它默认指向
一个Object 空对象(即称为: 原型对象)
原型对象
中有一个属性constructor
, 它指向函数对象
- 每个函数都有一个
给原型对象添加属性(一般都是方法)
- 作用:
函数的所有实例对象自动拥有原型中的属性(方法)
- 作用:
// 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
console.log(Date.prototype, typeof Date.prototype)
function Fun () {//alt + shift +r(重命名rename)
}
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 fun = new Fun()
fun.test() // test()
3.1.2 显式原型与隐式原型
- 每个函数
function
都有一个prototype
,即显式原型(属性)
- 每个
实例对象
都有一个__proto__
,可称为隐式原型(属性)
对象的隐式原型的值为其对应构造函数的显式原型的值
内存结构(图)
总结
- 函数的
prototype
属性: 在定义函数时自动添加
的,默认值是一个空 Object 对象
- 对象的
__proto__
属性:创建对象时自动添加
的,默认值为构造函数的 prototype 属性值
原型
对象即为当前实例
对象的父对象
- 程序员能
直接操作显式原型
, 但不能直接操作隐式原型(ES6之前)
- 函数的
//定义构造函数
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()
3.1.3 原型链
原型链(图解)
- 访问一个对象的属性时,
先在自身属性中查找,找到返回
如果没有, 再沿着__proto__这条链向上查找, 找到返回
如果最终没找到, 返回undefined
- 别名:
隐式原型链
- 作用:
查找对象的属性(方法)
- 访问一个对象的属性时,
// console.log(Object)
//console.log(Object.prototype)
console.log(Object.prototype.__proto__)
function Fn() {
this.test1 = function () {
console.log('test1()')
}
}
console.log(Fn.prototype)
Fn.prototype.test2 = function () {
console.log('test2()')
}
var fn = new Fn()
fn.test1()
fn.test2()
console.log(fn.toString())
console.log(fn.test3)
// fn.test3()
/*
1. 函数的显示原型指向的对象默认是空Object实例对象(但Object不满足)
*/
console.log(Fn.prototype instanceof Object) // true
console.log(Object.prototype instanceof Object) // false
console.log(Function.prototype instanceof Object) // true
/*
2. 所有函数都是Function的实例(包含Function)
*/
console.log(Function.__proto__===Function.prototype)
/*
3. Object的原型对象是原型链尽头
*/
console.log(Object.prototype.__proto__) // null
构造函数/原型/实体对象的关系(图解)
var o1 = new Object();
var o2 = {};
构造函数/原型/实体对象的关系2(图解)
function Foo(){ }
// 本质
var Foo = new Function(){}
Function = new Function()
所有函数的__proto__都是一样的
函数的显示原型指向的对象默认是空Object实例对象(但Object不满足)
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)
Object的原型对象是原型链尽头
console.log(Object.prototype.__proto__) // null
原型继承
构造函数的实例对象自动拥有构造函数原型对象的属性(方法)
利用的就是原型链
3.1.4 原型链属性问题
读取对象的属性值
时: 会自动到原型链中查找
设置对象的属性值
时:不会查找原型链
, 如果当前对象中没有此属性
,直接添加此属性并设置其值
方法一般定义在原型中
,属性一般通过构造函数定义在对象本身上
function Fn() {
}
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a, fn1) // xxx
var fn2 = new Fn()
fn2.a = 'yyy'
console.log(fn1.a, fn2.a, fn2) // xxx yyy
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) // Bob 12
var p2 = new Person('Jack', 12)
p2.setName('Cat')
console.log(p2) // Cat 12
console.log(p1.__proto__===p2.__proto__) // true
3.1.5 探索 instanceof
instanceof 是如何判断的?
- 表达式:
A instanceof B
A 是实例对象,B 是构造函数对象
如果 B 函数的显式原型对象在 A 对象的原型链上, 返回 true, 否则返回 false
- 表达式:
Function 是通过 new 自己产生的实例
/* 案例1 */
function Foo() { }
var f1 = new Foo()
console.log(f1 instanceof Foo) // true
console.log(f1 instanceof Object) // true
/* 案例2 */
console.log(Object instanceof Function) // true
console.log(Object instanceof Object) // true
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true
function Foo() {}
console.log(Object instanceof Foo) // false
3.1.6 原型面试题
/* 测试题1 */
function A () {
}
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 */
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() // b is not a function
F.a() // a()
F.b() // b()
console.log(f)
console.log(Object.prototype)
console.log(Function.prototype)
3.2 执行上下文与执行上下文栈
3.2.1 变量提升与函数提升
变量声明提升
- 通过
var
定义(声明)的变量,在定义语句之前就可以访问到
- 值:
undefined
- 通过
函数声明提升
- 通过
function
声明的函数,在之前就可以直接调用
- 值:
函数定义(对象)
- 通过
问题: 变量提升和函数提升是如何产生的?
/* 面试题 : 输出 undefined */
var a = 3
function fn () {
console.log(a)
var a = 4
}
fn() // undefined
console.log(b) //undefined 变量提升
fn2() //可调用 函数提升
// fn3() //不能 变量提升
var b = 3
function fn2() {
console.log('fn2()')
}
var fn3 = function () {
console.log('fn3()')
}
3.2.2 执行上下文
代码分类(位置)
全局
代码函数(局部)
代码
全局执行上下文
- 在执行全局代码前将
window
确定为全局执行上下文
- 对全局数据进行
预处理
var定义的全局变量==>undefined, 添加为window的属性
function声明的全局函数==>赋值(fun), 添加为window的方法
this==>赋值(window)
开始执行
全局代码
- 在执行全局代码前将
// 全局执行上下文
console.log(a1, window.a1) // undefined undefined
window.a2() // a2()
console.log(this) // window
var a1 = 3
function a2() {
console.log('a2()')
}
console.log(a1) // 3
函数执行上下文
- 在调用函数,
准备执行函数体之前
,创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
- 对
局部数据
进行预处理
形参变量==>赋值(实参)==>添加为执行上下文的属性
arguments==>赋值(实参列表), 添加为执行上下文的属性
var定义的局部变量==>undefined, 添加为执行上下文的属性
function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
this==>赋值(调用函数的对象)
开始执行
函数体代码
- 在调用函数,
// 函数(局部)执行上下文
function fn (a1){
console.log(a1) // 2
console.log(a2) // undefined
a3() // a3()
console.log(this) // window
console.log(arguments) // 2,3 伪数组
var a2 = 3
function a3(){
console.log('a3()')
}
}
fn(2,3)
3.2.3 执行上下文栈
- 在
全局代码执行前
,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在
全局执行上下文(window)确定
后,将其添加到栈中(压栈)
- 在
函数执行上下文创建后
,将其添加到栈中(压栈)
- 在
当前函数执行完后
,将栈顶的对象移除(出栈)
- 当
所有的代码执行完后
,栈中只剩下window
// 1. 进入全局执行上下文
var a = 10
var bar = function (x) {
var b = 5
// 3. 进入foo执行上下文
foo(x + b)
}
var foo = function (y) {
var c = 5
console.log(a + c + y)
}
// 2. 进入bar函数执行上下文
bar(10)
3.2.4 执行上下文面试题
<!--
1. 依次输出什么?
gb: undefined
fb: 1
fb: 2
fb: 3
fe: 3
fe: 2
fe: 1
ge: 1
2. 整个过程中产生了几个执行上下文? 5
-->
console.log('gb: '+ i) // i为undefined
var i = 1
foo(1)
function foo(i) {
if (i == 4) {
return
}
console.log('fb:' + i)
foo(i + 1) //递归调用: 在函数内部调用自己
console.log('fe:' + i)
}
console.log('ge: ' + i)
## 测试题1: 先执行变量提升, 再执行函数提升
function a() {}
var a
console.log(typeof a) // 'function'
## 测试题2:
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) // 报错
3.3 作用域与作用域链
3.3.1 作用域
理解
- 就是一块"地盘", 一个代码段所在的区域
- 它是
静态的(相对于上下文对象)
, 在编写代码时就确定了
分类
全局
作用域函数
作用域- 没有
块作用域
(ES6有了)
作用
隔离变量
,不同作用域下同名变量不会有冲突
/* //没块作用域
if(true) {
var c = 3
}
console.log(c)*/
var a = 10,
b = 20
function fn(x) {
var a = 100,
c = 300;
console.log('fn()', a, b, c, x)
function bar(x) {
var a = 1000,
d = 400
console.log('bar()', a, b, c, d, x)
}
bar(100) // bar() 1000 20 300 400 100
bar(200) // bar() 1000 20 300 400 200
}
fn(10) // fn() 100 20 300 10
3.3.2 作用域与执行上下文
区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
区别2
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
联系
- 执行上下文(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数使用域
var a = 10,
b = 20
function fn(x) {
var a = 100,
c = 300;
console.log('fn()', a, b, c, x)
function bar(x) {
var a = 1000,
d = 400
console.log('bar()', a, b, c, d, x)
}
bar(100) // bar() 1000 20 300 400 100
bar(200) // bar() 1000 20 300 400 200
}
fn(10) // fn() 100 20 300 10
3.3.3 作用域链
理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
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()
3.3.4 作用域面试题
var x = 10;
function fn() {
console.log(x); // 10
}
function show(f) {
var x = 20;
f();
}
show(fn); // 10
var fn = function () {
console.log(fn)
}
fn() // fn函数
var obj = {
fn2: function () {
console.log(fn2) // 找不到fn2
//console.log(this.fn2) // 能找到fn2
}
}
obj.fn2() // fn2 is not defined
3.4 闭包
3.4.1 闭包的概念
- MDN 对闭包的定义
- 闭包是指那些能够访问独立(自由)变量的函数(变量在本地使用,但定义在一个封闭的作用域中)换句话说,这些函数可以记忆它被创建时候的环境
- JavaScript权威指南(第6版)对闭包的定义
- 函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为闭包
- JavaScript高级程序设计(第3版)对闭包的定义
- 闭包是指有权访问另一个函数作用域中的变量的函数
- 阮一峰总结
- 闭包就是能够读取其他函数内部变量的函数
- 由于JavaScript语言中,只有函数内部的子函数才能读取局部变量
- 因此可以把闭包简单理解成定义在一个函数内部的函数
- 所以,在本质上,闭包就是将函数内部与函数外部连接起来的一座桥梁
function fun1(){
var n = 1
function fun2(){
console.log(n)
n++
}
return fun2
}
var f2 = fun1()
f2()
3.4.2 闭包的形成要素
- 在一个外部函数中定义了一个内部函数
- 内部函数使用了外部函数中的局部变量
- 内部函数被当做返回值传出
3.4.3 闭包的用途
- 可以读取函数内部的变量
- 让这些变量的值始终保持在内存中
3.4.4 常见的闭包的使用情形
- 自执行函数
// 普通函数
function fun1() {
console.log('aaa')
}
fun1()
// 自执行函数
(function(){
console.log('bbb')
})()
// 自执行函数,另一种写法
(function() {
console.log('ccc')
}())
// 自执行函数,传参数
(function(arg){
console.log(arg)
})('ddd')
// Jquery 类库的最外层
(function(window, undefined) {
// 省略的 Jquery 源码
window.jQuery = window.$ = jQuery
})(window)
- 模拟方法工厂
// 形成一个闭包应具备的3个要素
// 1. 在一个外层函数中定义过一个内部函数
// 2. 内部函数使用了外部函数中的局部变量
// 3. 内部函数被当做返回值传出
// 方法工厂的思路
// 给外部函数传不同的参数,让内部函数有不同的闭包环境,返回的内部函数因闭包不同而用途不同
// 加法器工厂
function makeAdder(captured) {
return function(free) {
var result = free + captured
console.log(result)
return result
}
}
var add10 = makeAdder(10)
add10(2)
add10(10)
- 模拟块级作用域
for (var i = 0; i < 5; i++) {
setTimeout(function(){
console.log(i)
}, 500)
}
// 改造
for (var i = 0; i < 5; i++) {
(function(num) {
setTimeout(function() {
console.log(num)
}, 500)
})(i)
}
// 进一步改造
for (var i = 0; i < 5; i++) {
print(i)
}
function print(num) {
setTimeout(function() {
console.log(num)
}, 500)
}
- 模拟私有变量
// 计数器的困境
var counter = 0
function add(){
console.log(++counter)
}
add()
add()
add()
// 改造
function add() {
var counter = 0
console.log(++counter)
}
add()
add()
add()
// 再次改造
function add() {
var counter = 0
var plus = function(){
console.log(++counter)
}
return plus
}
var plus = add()
plus()
plus()
plus()
- 模拟面向对象中的类
// 自定义对象
var Obj = function(name, age){
this.name = name
this._age = age
}
Obj.prototype.print = function(){
this._olden()
console.log(this.name + 'is' + this._age + 'years old')
}
Obj.prototype._olden = function(){
this._age++
}
var obj = new Obj('stone', 30)
obj.print()
// 使用闭包模拟面向对象中的类
var OopObj = (function(){
// 私有变量
var _age = 0
// 私有函数
function _olden(){
_age++
}
// 构造函数
var ReturnObj = function(name, age){
this.name = name
_age = age
}
// 公有方法
ReturnObj.prototype.print = function(){
_olden()
console.log(this.name + 'is' + _age + 'years old')
}
return ReturnObj
})()
var oopObj = new OopObj('stone', 30)
oopObj.print()
console.log(oopObj._age)
oopObj._olden()
3.4.5 闭包的内存回收
- 场景1
- inner 函数有使用outer函数中的局部变量,那么闭包中局部变量所占用的内存是否能被回收
- 结论
- 闭包中的局部变量被使用时,是绝对不能被回收的
<button id="btnGetInner">getInner</button>
<button id="btnRunInner">runInner</button>
<script>
var getLargeObj = function(size) {
var arr = []
var intSize = parseInt(size)
for(var i = intSize - 1; i >= 0; i--) {
for(var j = 199999; j >= 0; j--) {
arr.push('adfadlfhokdshf')
}
]
return arr
}
// outer 函数
function outer() {
console.log('outer')
var largeObject
if(!!window.ActiveXObject || "ActiveXObject" in window){
// 在 IE 浏览器下,监控内存的变化很方便,所以直接使用较大的内存,观察内存的变化曲线
largeObject = getLargeObj('100MB')
} else {
// 在 chrome 和 firefox 浏览器下,监控内存不方便,所以保存 1M 的内存快照,来对比起内存的前后变化
largeObject = getLargeObj('1MB')
}
// inner 函数
return function(){
console.log('inner')
return largeObject
}
}
document.getElementById('btnGetInner').onclick = function(){
inner = outer()
// 仅适用于 IE 浏览器
if(window.ConllectGarbage){
CollectGarbage()
}
}
document.getElementById('btnRunInner').onclick = function(){
if(inner) inner()
}
</script>
- 场景2
- inner 函数没有使用 outer 函数中的局部变量,那么闭包中局部变量所占用的内存是否能被回收
- 结论
- 闭包中的局部变量没有被使用,时可以被回收的
<button id="btnGetInner">getInner</button>
<button id="btnRunInner">runInner</button>
<script>
var getLargeObj = function(size) {
var arr = []
var intSize = parseInt(size)
for(var i = intSize - 1; i >= 0; i--) {
for(var j = 199999; j >= 0; j--) {
arr.push('adfadlfhokdshf')
}
]
return arr
}
// outer 函数
function outer() {
console.log('outer')
var largeObject
if(!!window.ActiveXObject || "ActiveXObject" in window){
// 在 IE 浏览器下,监控内存的变化很方便,所以直接使用较大的内存,观察内存的变化曲线
largeObject = getLargeObj('100MB')
} else {
// 在 chrome 和 firefox 浏览器下,监控内存不方便,所以保存 1M 的内存快照,来对比起内存的前后变化
largeObject = getLargeObj('1MB')
}
// inner 函数
return function(){
console.log('inner')
// return largeObject
}
}
document.getElementById('btnGetInner').onclick = function(){
inner = outer()
// 仅适用于 IE 浏览器
if(window.ConllectGarbage){
CollectGarbage()
}
}
document.getElementById('btnRunInner').onclick = function(){
if(inner) inner()
}
</script>
- 场景3
- outer函数中有多个局部变量,其中只有部分被inner函数所使用,那么闭包中局部变量所占用的内存是否能被回收
- 结论
- 如果闭包中的多个局部变量,被使用局部变量,是不能被回收的,其余的局部变量,仍然可以被回收
<button id="btnGetInner">getInner</button>
<button id="btnRunInner">runInner</button>
<script>
var getLargeObj = function(size) {
var arr = []
var intSize = parseInt(size)
for(var i = intSize - 1; i >= 0; i--) {
for(var j = 199999; j >= 0; j--) {
arr.push('adfadlfhokdshf')
}
]
return arr
}
// outer 函数
function outer() {
console.log('outer')
var largeObject
if(!!window.ActiveXObject || "ActiveXObject" in window){
// 在 IE 浏览器下,监控内存的变化很方便,所以直接使用较大的内存,观察内存的变化曲线
largeObject = getLargeObj('100MB')
largeObject2 = getLargeObj('100MB')
} else {
// 在 chrome 和 firefox 浏览器下,监控内存不方便,所以保存 1M 的内存快照,来对比起内存的前后变化
largeObject = getLargeObj('1MB')
largeObject2 = getLargeObj('1MB')
}
// inner 函数
return function(){
console.log('inner')
return largeObject
}
}
document.getElementById('btnGetInner').onclick = function(){
inner = outer()
// 仅适用于 IE 浏览器
if(window.ConllectGarbage){
CollectGarbage()
}
}
document.getElementById('btnRunInner').onclick = function(){
if(inner) inner()
}
</script>
- 场景4
- inner 函数并没有使用 outer 函数中的局部变量,但 inner 函数中直接使用了 eval 函数,那么闭包中局部变量所占用的内存是否能被回收
- 结论
- 如果返回的 inner 函数中有直接使用 eval 函数,则会保留闭包中所有局部变量的绑定,以避免产生不可预期的结果,所以他们都不能被回收
<button id="btnGetInner">getInner</button>
<button id="btnRunInner">runInner</button>
<script>
var getLargeObj = function(size) {
var arr = []
var intSize = parseInt(size)
for(var i = intSize - 1; i >= 0; i--) {
for(var j = 199999; j >= 0; j--) {
arr.push('adfadlfhokdshf')
}
]
return arr
}
// outer 函数
function outer() {
console.log('outer')
var largeObject
if(!!window.ActiveXObject || "ActiveXObject" in window){
// 在 IE 浏览器下,监控内存的变化很方便,所以直接使用较大的内存,观察内存的变化曲线
largeObject = getLargeObj('100MB')
largeObject2 = getLargeObj('100MB')
} else {
// 在 chrome 和 firefox 浏览器下,监控内存不方便,所以保存 1M 的内存快照,来对比起内存的前后变化
largeObject = getLargeObj('1MB')
largeObject2 = getLargeObj('1MB')
}
// inner 函数
return function(){
console.log('inner')
eval('')
// return largeObject
}
}
document.getElementById('btnGetInner').onclick = function(){
inner = outer()
// 仅适用于 IE 浏览器
if(window.ConllectGarbage){
CollectGarbage()
}
}
document.getElementById('btnRunInner').onclick = function(){
if(inner) inner()
}
</script>
- 场景5
- inner 函数并没有使用 outer 函数中的局部变量,但 inner 函数中直接使用了 window.eval 函数,那么闭包中局部变量所占用的内存是否能被回收
- 结论
- 由于 window.eval 方法将在全局作用域下执行,所以 window.eval 是无法访问到 outer 函数中的局部变量,所以此时的内存可以被回收
<button id="btnGetInner">getInner</button>
<button id="btnRunInner">runInner</button>
<script>
var getLargeObj = function(size) {
var arr = []
var intSize = parseInt(size)
for(var i = intSize - 1; i >= 0; i--) {
for(var j = 199999; j >= 0; j--) {
arr.push('adfadlfhokdshf')
}
]
return arr
}
// outer 函数
function outer() {
console.log('outer')
var largeObject
if(!!window.ActiveXObject || "ActiveXObject" in window){
// 在 IE 浏览器下,监控内存的变化很方便,所以直接使用较大的内存,观察内存的变化曲线
largeObject = getLargeObj('100MB')
largeObject2 = getLargeObj('100MB')
} else {
// 在 chrome 和 firefox 浏览器下,监控内存不方便,所以保存 1M 的内存快照,来对比起内存的前后变化
largeObject = getLargeObj('1MB')
largeObject2 = getLargeObj('1MB')
}
// inner 函数
return function(){
console.log('inner')
window.eval('')
// return largeObject
}
}
document.getElementById('btnGetInner').onclick = function(){
inner = outer()
// 仅适用于 IE 浏览器
if(window.ConllectGarbage){
CollectGarbage()
}
}
document.getElementById('btnRunInner').onclick = function(){
if(inner) inner()
}
</script>
- 场景6
- inner 函数并没有使用 outer 函数中的局部变量,但另一个内部函数 help 函数访问了 outer 函数中的局部变量或者直接调用 eval 函数,那么闭包中局部变量所占用的内存是否能被回收
- 结论
- 不仅是被返回的 inner 函数,在其他的内部函数中使用了 outer 函数中的局部变量或直接调用 eval 函数,也同样会造成内存无法回收
<button id="btnGetInner">getInner</button>
<button id="btnRunInner">runInner</button>
<script>
var getLargeObj = function(size) {
var arr = []
var intSize = parseInt(size)
for(var i = intSize - 1; i >= 0; i--) {
for(var j = 199999; j >= 0; j--) {
arr.push('adfadlfhokdshf')
}
]
return arr
}
// outer 函数
function outer() {
console.log('outer')
var largeObject
if(!!window.ActiveXObject || "ActiveXObject" in window){
// 在 IE 浏览器下,监控内存的变化很方便,所以直接使用较大的内存,观察内存的变化曲线
largeObject = getLargeObj('100MB')
largeObject2 = getLargeObj('100MB')
} else {
// 在 chrome 和 firefox 浏览器下,监控内存不方便,所以保存 1M 的内存快照,来对比起内存的前后变化
largeObject = getLargeObj('1MB')
largeObject2 = getLargeObj('1MB')
}
function help() {
return largeObject
}
// inner 函数
return function(){
console.log('inner')
window.eval('')
// return largeObject
}
}
document.getElementById('btnGetInner').onclick = function(){
inner = outer()
// 仅适用于 IE 浏览器
if(window.ConllectGarbage){
CollectGarbage()
}
}
document.getElementById('btnRunInner').onclick = function(){
if(inner) inner()
}
</script>
- 影响闭包内存被回收的因素
- 内部函数是否使用了外部函数的局部变量
- 内部函数是否直接调用了eval函数
- 是否应该使用闭包
- 在最新的IE,Chrome,firefox浏览器下,是可以使用闭包的
- 而在旧版本的浏览器中,特别是旧版本的IE中,就不建议使用闭包了
- 如何高效的使用闭包
- 内部函数尽量的少使用外部函数的局部变量
- 内部函数尽量不要直接调用eval函数
- 在退出函数之前,将不使用的局部变量全部删除或设置为null,断开变量和内存之间的联系
3.4.2 闭包理解
如何产生闭包?
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
闭包到底是什么?
- 使用chrome调试查看
- 理解一: 闭包是嵌套的内部函数(绝大部分人)
- 理解二: 包含被引用变量(函数)的对象(极少数人)
- 注意: 闭包存在于嵌套的内部函数中
产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
function fn1 () {
var a = 2
var b = 'abc'
function fn2 () { //执行函数定义就会产生闭包(不用调用内部函数)
console.log(a)
}
fn2()
}
fn1() // 2
function fun1() {
var a = 3
var fun2 = function () {
console.log(a)
}
}
fun1()
3.4.3 常见的闭包
将函数作为另一个函数的返回值
将函数作为实参传递给另一个函数调用
// 1. 将函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
// 2. 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function () {
alert(msg)
}, time)
}
showDelay('atguigu', 2000)
3.4.4 闭包的作用
- 使用函数内部的变量在函数执行完后, 仍然存活在内存中(
延长了局部变量的生命周期
) - 让函数外部可以
操作
(读写)到函数内部的数据
(变量/函数) - 问题:
- 函数执行完后, 函数内部声明的局部变量是否还存在? 一般是
不存在
,存在于闭中的变量才可能存在
- 在函数外部能直接访问函数内部的局部变量吗?
不能
, 但我们可以通过闭包让外部操作它
- 函数执行完后, 函数内部声明的局部变量是否还存在? 一般是
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
// return a
}
function fn3() {
a--
console.log(a)
}
return fn3
}
var f = fn1()
f() // 1
f() // 0
3.4.5 闭包的生命周期
产生
- 在
嵌套内部函数定义执行完时
就产生了(不是在调用)
- 在
死亡
- 在
嵌套的内部函数成为垃圾对象时
- 在
function fn1() {
//此时闭包就已经产生了(函数提升, 内部函数对象已经创建了)
var a = 2
function fn2 () {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)
3.4.6 闭包的应用——自定义JS模块
-
定义JS模块
- 具有
特定功能
的js文件 - 将
所有的数据和功能都封装在一个函数内部(私有的)
只向外暴露一个包含n个方法的对象或函数
- 模块的使用者,
只需要通过模块暴露的对象调用方法来实现对应的功能
- 具有
-
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
}
}
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule()
module.doSomething()
module.doOtherthing()
</script>
================================================================
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
}
})()
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
myModule2.doSomething()
myModule2.doOtherthing()
</script>
3.4.7 闭包的缺点及解决
缺点
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 容易造成内存泄露
解决
- 能不用闭包就不用
- 及时释放
function fn1() {
var arr = new Array[100000]
function fn2() {
console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
f = null //让内部函数成为垃圾对象-->回收闭包
3.4.8 内存溢出与内存泄露
内存溢出
- 一种程序运行出现的错误
- 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
内存泄露
- 占用的内存没有及时释放
- 内存泄露积累多了就容易导致内存溢出
常见的内存泄露
- 意外的全局变量
- 没有及时清理的计时器或回调函数
- 闭包
// 1. 内存溢出
var obj = {}
for (var i = 0; i < 10000; i++) {
obj[i] = new Array(10000000)
console.log('-----')
}
// 2. 内存泄露
// 意外的全局变量
function fn() {
a = new Array(10000000)
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
3.4.9 闭包面试题
//代码片段一
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 有闭包
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
4. 面向对象
4.1 对象简介
对象
属于一种复合的数据类型
,在对象中可以保存多个不同数据类型的属性
4.2 对象的分类
内建对象
- 由
ES 标准
中定义的对象,在任何的 ES 的实现中都可以使用
- 比如:
Math String Number Boolean Function Object...
- 由
宿主对象
- 由
JS 的运行环境
提供的对象,目前来讲主要指由浏览器提供的对象
- 比如
BOM DOM
- 由
自定义对象
- 由开发人员自己创建的对象
4.3 对象的属性名和属性值
属性名
- 对象的属性名
不强制要求遵守标识符的规范
- 但还是尽量按照标识符的规范去做
- 对象的属性名
- 如果要使用
特殊的属性名
,不能采用 . 的方式来操作
- 需要使用另一种方式
- 语法:
对象["属性名"] = 属性值
读取时也需要采用这种方式
- 使用
[]
这种形式去操作属性,更加的灵活
- 在
[]
中可以直接传递一个变量
,这样变量值是多少就会读取那个属性
var obj = new Object()
obj.name = 'zs'
obj.var = 'hello'
console.log(obj.var)
obj["123"] = 789
var n = 'nihao'
console.log(obj["123"])
属性值
- JS 对象的属性值,可以是
任意的数据类型
- 甚至也可以是一个
对象
- JS 对象的属性值,可以是
in 运算符
- 通过该运算符可以
检查一个对象中是否含有指定的属性
如果有则返回 true, 没有则返回 false
- 语法
"属性名" in 对象
- 通过该运算符可以
obj.test = true
obj.test = null
obj.test = undefined
// 创建一个对象
var obj2 = new Object()
obj2.name = 'zs'
// 将 obj2 设置为 obj 的属性
obj.test = obj2
console.log(obj.test.name) // zs
console.log(obj.test2) // undefined
// 检查 obj 中是否含有 test2 属性
console.log("test2", in obj) // false
4.4 对象字面量
- 使用对象字面量,可以
在创建对象时,直接指定对象中的属性
- 语法:
{属性名: 属性值,属性名: 属性值...}
对象字面量的属性名可以加引号也可以不加
,建议不加- 如果要
使用一些特殊的名字,则必须加引号
- 属性名和属性值是一组一组的
名值对结构
- 名和值之间使用
:
连接,多个名值对之间使用,
隔开 - 如果
一个属性之后没有其他的属性
了,就不要写,
// 创建一个对象
var obj = new Object()
// 使用对象字面量来创建一个对象
var obj = {}
// console.log(typeof obj) // object
obj.name = 'zs'
// console.log(obj.name)
var obj2 = {
name: 'zs',
age: 23,
gender: '男',
test: {name: 'zd'}
}
console.log(obj2.test)
5. 面向对象高级
5.1 对象创建模式
5.1.1 Object 构造函数模式
Object构造函数模式
套路
: 先创建空Object对象, 再动态添加属性/方法适用场景
: 起始时不确定对象内部数据问题
: 语句太多
/*
一个人: name:"Tom", age: 12
*/
// 先创建空Object对象
var p = new Object()
p = {} //此时内部数据是不确定的
// 再动态添加属性/方法
p.name = 'Tom'
p.age = 12
p.setName = function (name) {
this.name = name
}
//测试
console.log(p.name, p.age)
p.setName('Bob')
console.log(p.name, p.age)
5.1.2 对象字面量
对象字面量模式
套路
: 使用{}创建对象, 同时指定属性/方法适用场景
: 起始时对象内部数据是确定的问题
: 如果创建多个对象, 有重复代码
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
}
}
5.1.3 工厂模式
工厂模式
套路
: 通过工厂函数动态创建对象并返回适用场景
: 需要创建多个对象问题
: 对象没有一个具体的类型, 都是Object类型
function createPerson(name, age) { //返回一个对象的函数===>工厂函数
var obj = {
name: name,
age: age,
setName: function (name) {
this.name = name
}
}
return obj
}
// 创建2个人
var p1 = createPerson('Tom', 12)
var p2 = createPerson('Bob', 13)
// p1/p2是Object类型
function createStudent(name, price) {
var obj = {
name: name,
price: price
}
return obj
}
var s = createStudent('张三', 12000)
// s也是Object
5.1.4 自定义构造函数模式
自定义构造函数模式
套路
: 自定义构造函数, 通过new创建对象适用场景
: 需要创建多个类型确定的对象问题
: 每个对象都有相同的数据, 浪费内存
//定义类型
function Person(name, age) {
this.name = name
this.age = age
this.setName = function (name) {
this.name = name
}
}
var p1 = new Person('Tom', 12)
p1.setName('Jack')
console.log(p1.name, p1.age)
console.log(p1 instanceof Person)
function Student (name, price) {
this.name = name
this.price = price
}
var s = new Student('Bob', 13000)
console.log(s instanceof Student)
var p2 = new Person('JACK', 23)
console.log(p1, p2)
5.1.5 构造函数+原型的组合模式
构造函数+原型的组合模式
套路
: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上适用场景
: 需要创建多个类型确定的对象
function Person(name, age) { //在构造函数中只初始化一般函数
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 23)
var p2 = new Person('Jack', 24)
console.log(p1, p2)
5.2 继承模式
5.2.1 原型链继承
原型链继承
套路
- 定义父类型构造函数
- 给父类型的原型添加方法
- 定义子类型的构造函数
- 创建父类型的对象赋值给子类型的原型
- 将子类型原型的构造属性设置为子类型
- 给子类型原型添加方法
- 创建子类型的对象: 可以调用父类型的方法
关键
- 子类型的原型为父类型的一个实例对象
//父类型
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
5.2.2 借用构造函数继承
借用构造函数继承(假的)
套路
:- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造
关键
:- 在子类型构造函数中通用call()调用父类型构造函数
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)
5.2.3 组合继承
原型链+借用构造函数的组合继承
- 利用原型链实现对父类型对象的方法继承
- 利用super()借用父类型构建函数初始化相同属性
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 //修正constructor属性
Student.prototype.setPrice = function (price) {
this.price = price
}
var s = new Student('Tom', 24, 15000)
s.setName('Bob')
s.setPrice(16000)
console.log(s.name, s.age, s.price)
6. 线程机制与事件机制
6.1 进程与线程
进程
程序的一次执行
, 它占有一片独有的内存空间
线程
- 是进程内的一个
独立执行单元
- 是程序执行的一个
完整流程
CPU
的最小基本调度单位
- 是进程内的一个
6.3 图解
6.4 相关知识
进程与线程
- 应用程序必须运行在
某个进程的某个线程上
- 一个进程中一般
至少有一个运行的线程: 主线程
,进程启动后自动创建
- 一个进程中
也可以同时运行多个线程
, 我们会说程序是多线程
运行的 一个进程内的数据可以供其中的多个线程直接共享
多个进程之间的数据是不能直接共享的
- 应用程序必须运行在
线程池(thread pool)
:保存多个线程对象的容器, 实现线程对象的反复利用
6.5 相关问题
6.5.1 何为多进程与多线程?
多进程运行
: 一应用程序可以同时启动多个实例运行多线程
: 在一个进程内, 同时有多个线程运行
6.5.2 比较单线程与多线程?
多线程
优点
- 能有效提升CPU的利用率
缺点
- 创建多线程开销
- 线程间切换开销
- 死锁与状态同步问题
单线程
优点
- 顺序编程简单易懂
缺点
- 效率低
6.5.3 JS是单线程还是多线程?
- js是单线程运行的
- 但使用H5中的 Web Workers可以多线程运行
6.5.4 浏览器运行是单进程还是多进程?
单进程
firefox
老版IE
多进程
chrome
新版IE
6.5.5 浏览器运行是单线程还是多线程?
都是多线程
6.2 浏览器内核
6.2.1 什么是浏览器内核?
支持浏览器运行的最核心的程序
6.2.2 不同的浏览器可能不太一样
Chrome, Safari
: webkitfirefox
: GeckoIE
: Trident360,搜狗等国内浏览器
: Trident + webkit
6.2.3 内核由很多模块组成
主线程
js引擎模块
: 负责js程序的编译与运行html,css文档解析模块
: 负责页面文本的解析dom/css模块
: 负责dom/css在内存中的相关处理布局和渲染模块
: 负责页面的布局和效果的绘制
分线程
定时器模块
: 负责定时器的管理网络请求模块
: 负责服务器请求(常规/Ajax)DOM事件响应模块
: 负责事件的管理
6.3 定时器引发的思考
定时器真是定时执行的吗?
- 定时器
并不能保证真正定时执行
- 一般会
延迟一丁点(可以接受)
,也有可能延迟很长时间(不能接受)
- 定时器
定时器回调函数是在分线程执行的吗?
- 在
主线程
执行的,js是单线程的
- 在
定时器是如何实现的?
事件循环模型
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 < 1000000000; i++) {
}
}
6.4 JS是单线程的
如何证明js执行是单线程的?
- setTimeout()的回调函数是在
主线程
执行的 - 定时器回调函数
只有在运行栈中的代码全部执行完后才有可能执行
- setTimeout()的回调函数是在
为什么js要用单线程模式, 而不用多线程模式?
- JavaScript的单线程,与它的用途有关
- 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM
- 这决定了它只能是单线程,否则会带来很复杂的同步问题
代码的分类
- 初始化代码
- 回调代码
js引擎执行代码的基本流程
先执行初始化代码
: 包含一些特别的代码 回调函数(异步执行)- 设置定时器
- 绑定事件监听
- 发送ajax请求
后面在某个时刻才会执行回调代码
setTimeout(function () {
console.log('timeout 2222')
alert('22222222') // 当弹出111111 1秒后弹出2222222
}, 2000)
setTimeout(function () {
console.log('timeout 1111')
alert('1111111') // 当弹出-----1秒后弹出111111
}, 1000)
setTimeout(function () {
console.log('timeout() 00000')
}, 0)
function fn() {
console.log('fn()')
}
fn()
console.log('alert()之前')
alert('------') //暂停当前主线程的执行, 同时暂停计时, 点击确定后, 恢复程序执行和计时
console.log('alert()之后')
6.5 浏览器的事件循环模型
所有代码分类
初始化执行代码(同步代码)
: 包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码回调执行代码(异步代码)
: 处理回调逻辑
js引擎执行代码的基本流程
:初始化代码===>回调代码
- 模型的2个
重要组成部分
:事件(定时器/DOM事件/Ajax)管理模块
回调队列
模型的运转流程
- 执行初始化代码, 将事件回调函数交给对应模块管理
- 当事件发生时, 管理模块会将回调函数及其数据添加到回调列队中
- 只有当初始化代码执行完后(可能要一定时间), 才会遍历读取回调队列中的回调函数执行
6.5.1 模型原理图
6.5.2 相关重要概念
执行栈
- execution stack
- 所有的代码都是在此空间中执行的
浏览器内核
- browser core
- js引擎模块(在主线程处理)
- 其它模块(在主/分线程处理)
同一个: callback queue
任务队列
- task queue
消息队列
- message queue
事件队列
- event queue
事件轮询
- event loop
- 从任务队列中循环取出回调函数放入执行栈中处理(一个接一个)
事件驱动模型
- event-driven interaction model
请求响应模型
- request-response model
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()
6.6 Web Workers(多线程)
6.6.1 Web Workers 介绍
H5规范提供了js分线程的实现, 取名为: Web Workers
- 我们可以将一些大计算量的代码交由web Worker运行而不冻结用户界面
- 但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质
6.6.2 Web Workers 使用
- 创建在分线程执行的js文件
var onmessage =function (event){ //不能用函数声明
console.log('onMessage()22');
var upper = event.data.toUpperCase();//通过event.data获得发送来的数据
postMessage( upper );//将获取到的数据发送会主线程
}
- 在主线程中的js中发消息并设置回调
//创建一个Worker对象并向它传递将在新线程中执行的脚本的URL
var worker = new Worker("worker.js");
//接收worker传过来的数据函数
worker.onmessage = function (event) {
console.log(event.data);
};
//向worker发送数据
worker.postMessage("hello world");
6.6.3 Web Workers 图解
6.6.4 相关API
- Worker: 构造函数, 加载分线程执行的js文件
- Worker.prototype.onmessage: 用于接收另一个线程的回调函数
- Worker.prototype.postMessage: 向另一个线程发送消息
6.6.5 应用练习
直接在主线程
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
// 1 1 2 3 5 8 f(n) = f(n-1) + f(n-2)
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用
}
// console.log(fibonacci(7))
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value
var result = fibonacci(number)
alert(result)
}
</script>
使用Worker在分线程
<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>
6.6.6 不足
- 慢
- worker内代码不能操作DOM(更新UI)
- 不能跨域加载JS
- 不是每个浏览器都支持这个新特性
7. 深入了解 JavaScript 内存管理
7.1 GC 概念
- JavaScript 有
垃圾回收机制
,英文简称GC,英文全称Garbage Collection 执行环境会负责管理代码执行过程中使用的内存
- 开发人员不用再关心内存使用问题,
垃圾回收器会按照固定的时间间隔,周期性的执行这一操作
7.2 内存的生命周期
7.2.1 内存的分配场景
简单数据类型
var name = "stone"; // 字符串
var age = 30; // 数值型
var maritalStatus = true; // 字符串
var hobbies = null; // 空值,null 是程序级的值的空缺,正常或意料之中的值的空缺
var education; // 未定义,undefined 是系统级的值的空缺,出乎意料的值的空缺
基本包装类型
var numberObject = new Number(30);
var stringObject = new String("stone");
var booleanObject = new Boolean(true);
对象类型
var obj1 = new Object();
var obj2 = {a: 4, b: 5}; // 推荐写法
var obj3 = Object.create();
var obj4 = new MyObject(); // 自定义对象
var obj5 = document.getElementById("btn") // DOM 对象
数组类型
var arr1 = new Array();
var arr2 = [1,2,3]; // 推荐写法
函数类型
var fun1 = function () {} // 推荐
var fun2 = new Function(code)
其他常用对象
var date = new Date(); // 时间对象
var error = new Error(); // 错误对象
var expression = / pattern / flags; // 正则表达式
var expression2 = new RegExp(patten, flags); // 正则表达式
闭包
function outer(name) {
var x = name;
return function inner() {
return "Hi," + x;
}
}
7.2.2 内存的回收原则
全局变量
- 当页面被关闭,才会被回收
局部变量
- 当函数执行完,才会被回收
闭包
- 局部变量的特殊情况,局部变量会被内部函数所占用,就算[外部函数执行完],也不一定会被回收
var getLargeObj = function(size) {
var arr = [];
var intSize = parseInt(size);
for(var i = intSize - 1; i >= 0; i--) {
for(var j = intSize - 1; j >= 0; j--) {
arr.push('aosdfhaodhufao;udhf');
}
}
return arr;
}
// 全局变量,当页面被关闭,才会被回收
var largeObj1 = null;
function func1() {
largeObj1 = getLargeObj("300MB")
}
// 局部变量,当函数执行完,才会被回收
function func2() {
var largeObj2 = getLargeObj("300MB")
}
7.3 GC 的原理及性能优化
7.3.1 标记清除(mark-and-sweep)
- 当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为
[进入环境]
从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们
- 而当变量离开环境时,则将其标记为
[离开环境]
var a = 10; // 被标记,进入环境
function test() {
var b = 20; // 被标记,进入环境
var c = 30; // 被标记,进入环境
}
test(); // 执行完毕之后,b、c 离开环境,等待被回收
主要缺点
- 某些对象被清理后,内存是不连续的,那么就算内存占用率不高,例如还有50%,但是由于内存空隙太多,后来的对象可能无法存储到内存之中
解决方案
- 在垃圾回收后进行整理操作,这种方法也叫标记整理,整理的过程就是将不连续的内存向一端复制,使不连续的内存连续起来
- 在垃圾回收后进行整理操作,这种方法也叫标记整理,整理的过程就是将不连续的内存向一端复制,使不连续的内存连续起来
7.3.2 引用计数(reference counting)
- 引用计数的含义是
- 跟踪记录每个值被引用的次数
- 当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1
- 如果同一个值又被赋给另一个变量,则该值的引用次数加1
- 相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1
- 当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收
- 这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为零的值所占用的内存
function test() {
var a = {}; // a的引用次数为0
var b = a; // a的引用次数为1
var c = a; // a的引用次数为2
b = {}; // a的引用次数为1
}
function problem() {
var a = {}; // a的引用次数为0
var b = {}; // b的引用次数为0
a.prop = b; // b的引用次数为1
b.prop = a; // a的引用次数为1
}
- 低版本的 IE(IE678)中有一部分对象并不是原生 JavaScript 对象
- 其中BOM和DOM对象就是使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略
- 因此,即时IE的JavaScript引擎时使用标记清除策略来实现的,但JavaScript访问的COM对象依然是基于引用次数策略的
- 只要在IE中涉及COM对象,就会存在循环引用的问题
var element = document.getElementById("btn_test");
var obj = {}
obj.element = element;
element.prop = obj;
obj.element = null;
element.prop = null;
7.3.3 解除引用
- 一旦数据不再有用,最好通过将其值设置为null来释放其引用——这个做法叫做解除引用
- 这一做法适用于大多数全局变量和全局对象的属性
- 局部变量会在它们离开执行环境时自动被解除引用
- 解除一个值的引用并不意味着自动回收该值所占用的内存
- 解除引用的真正作用时让值脱离执行环境,以便垃圾收集器下次运行时将其回收
function createPerson(name) {
var localPerson = {};
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson('Nicholas');
// 手工解除引用
globalPerson = null;
7.3.4 GC 的优化策略
- JavaScript 的垃圾回收策略也无法避免一个问题
- 垃圾回收时,会停止响应其他操作,这是为了安全考虑
- 而JavaScript的垃圾回收在100ms甚至以上,对一般的应用还好,但对于JavaScript游戏和动画,这种对连贯性要求比较高的应用,就麻烦了
- 这就是引擎需要优化的点:避免垃圾回收造成的长时间停止响应
- 解决方案
- 分代回收(Generation GC)
- 增量回收(Incremental GC)
- 分代回收
- 目的是通过区分[新生]与[持久]对象
- 多回收[新生对象区(young generation)]
- 少回收[持久对象区(tenurend generation)]
- 减少每次需遍历的对象,从而减少每次GC的耗时
- 分代回收是基于以下三条假设所设计的
- 对象越新,生存期越短
- 对象越老,生存期越长
- 回收内存的一部分,速度快于回收整个内存
- 增量回收
- 就是[每次处理一点,下次再处理一点,如此类推]
- 它会将完整的回收过程拆分成很多部分,没做完一部分就停下来,让JavaScript的应用逻辑执行一会,这样垃圾回收与应用逻辑交替完成
- 经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原来的1/6左右
- 惰性清理
- 当标记完成后,所有的对象都已经被标记为[进入环境]或[离开环境],堆上多少空间格局已经确定
- 我们可以不必着急释放那些无效对象所占用的空间,延迟的清理过程,等到需要分配内存时再来清理,这样可以进一步提高GC的效率
7.4 内存的调试技巧
7.4.1 IE 的内存调试技巧
- 在IE浏览器下,我们可以通过开发者工具的[Memory]选项卡,来监控内存的使用情况、新建堆快照查看中各个对象占用内存情况、以及对比多个堆快照的区别,来快速的定位到内存泄露问题
- IE浏览器的内存管理工具虽然功能不多,但特别简洁实用,容易上手
7.4.2 Chrome 的内存调试技巧
- 在 Chrome 浏览器下,通过开发者工具的[Timeline]选项卡,监控内存的使用情况、事件的耗时、以及GC回收的内存大小和耗时
- 通过[Profiles]选项卡,新建堆快照查看中各个对象占用的内存情况、以及对比多个堆快照的区别
- Chrome的内存管理工具比IE记录了更多信息,可通过这些信息探究更深层次的内存泄露问题
7.4.3 FireFox 的内存调试技巧
- 在FireFox浏览器下,我们可以通过[性能]选项卡,监控事件和GC的耗时
- 通过[内存]选项卡,新建堆快照、对比多个堆快照的区别,但给出的信息较少
8. this 九重关
8.1 this 的概念
- this在JavaScript中指的是函数执行时的上下文,跟函数定义时的上下文无关
- 随着函数使用 场合的不同,this的值会发生变化
- 但是有一个总的原则,那就是this指代的是调用函数的那个对象
8.2 全局作用域中的 this
// 以下测试仅适用于浏览器环境下,而非 nodejs 环境
// 1. 全局作用域中的 this
// 全局作用域中,也就是在任何函数体的外部,this指代的是全局对象 window
console.log(this)
console.log(this === window)
8.3 函数作用域中的 this
// this 指代的是调用函数的那个对象
function f1(){
console.log(this)
}
f1()
window.f1()
function f2(){
'use strict'
console.log(this)
}
f2()
8.4 对象方法中的 this
// this 是动态绑定的
var o = {
name: 'stone',
f: function(){
console.log(this.name)
}
}
o.f()
function f3(){
console.log(this.name)
}
var o2 = {
name: 'stone'
}
o2.f = f3
o2.f()
8.5 闭包中的 this
function f4(){
var name = 'stone'
var f5 = function(){
console.log(name)
console.log(this)
}
return f5
}
f4()()
var f6 = f4()
f6()
8.6 eval 函数中的 this
function f7() {
// return eval('this')
return this
}
var o3 = {
name: 'stone',
f: function() {
// return eval('this.name')
return this.name
}
}
console.log(f7())
console.log(o3.f())
8.7 call() 和 apply() 中的 this
// this 是指向 call 和 apply 方法的第一个参数,如果参数为空,则默认指向 window 对象
var x = 0
var f8 = function() {
console.log(this.x)
}
var o4 = {
x: 1,
f: f8
}
f8() // 0
f8.call(o4) // 1
o4.f() // 1
o4.f.call() // 0
8.8 bind() 中的 this
// this 将永久地绑定到了 bind 的第一个参数
function f9(){
console.log(this.a)
}
var f10 = f9.bind({ a: 0})
f10() // 0
var o5 = {
a: 2,
b: f9,
c: f10
}
o5.b() // 2
o5.c() // 0
8.9 DOM 事件处理函数中的 this
DOM 事件处理函数中的 this <br>
当函数使用 addEventListener 作为事件处理函数时,它的 this 指向触发事件的元素(非IE浏览器)。<br>
当函数使用 attachEvent 作为事件处理函数时,它的 this 指向 window (IE浏览器)。<br>
<button id="btn">click</button>
<script>
var btn = document.getElementById('btn')
if(btn.addEventListener){
// chrome
btn.addEventListener('click', function(){
console.log(this)
})
} else {
// IE
btn.attachEvent('onclick', function(){
console.log(this)
})
}
</script>
8.10 内联事件处理函数中的 this
内联事件处理函数中的 this <br>
当代码被内联处理函数调用时,它的 this 指向的是触发事件的 DOM 元素<br>
<button onclick="alert(this.tagName">click</button>
<button onclick="alert((function(){return this === window})())">click</button>