1. 深入
1.1. 数据
1.1.1. 分类
1.1.1.1 基本类型
- String: 任意字符串
- Number: 任意数字
- boolean: true/false
- undefined: undefined(定义了变量但未赋值)
- null: null(定义了变量且赋值为
null
)
1.1.1.2 对象类型
- Object: 任意对象
- Function: 一种特别的对象,可以执行
- Array: 一种特别的对象,拥有数值下标,内部数据是有序的
1.1.2. 判断
1.1.2.1. 基本类型
- typeof: 返回数据类型的字符串表达;可以判断
undefined
、数值、字符串、布尔值、function
,不能判断null
与object
(都是object
)、object
与array
(都是object
) - ===: 全等,不做数据转换;可以判断
boolean
、undefined
、null
var a;
console.log(a, typeof a); // undefined 'undefined'
console.log(typeof a === 'undefined', a === undefined); // true true
console.log('undefined' == undefined); // false
1.1.2.2. 对象类型
- instanceof: 判断对象的具体类型
var b1 = {
b2: [1, 'abc', console.log],
b3: function(){
console.log('b3')
}
};
console.log(b1 instanceof Object, b1 instanceof Array); // true false
console.log(b1.b2 instanceof Object, b1.b2 instanceof Array); // true true
console.log(b1.b3 instanceof Object, b1.b3 instanceof Function); // true true
console.log(typeof b1.b3 === 'function'); // true
1.1.3. tips
- 何时将变量赋值为null
var b = null;
初始化时赋值为null,表明该变量将要赋值为对象;
程序结束时赋值为null,让原先指向的对象成为垃圾对象,被垃圾回收器回收,释放内存; - 严格区分变量类型和数据类型
数据类型: 基本类型 & 对象类型
变量类型(变量内存值的类型): 基本类型(保存基本类型的数据) & 引用类型(保存地址值)
1.1.4. 数据 变量 内存
1.1.4.1. 数据
存储在内存中代表特定信息的东西,本质上是0101...
,是内存中所有操作的目标:算术运算、逻辑运算、赋值、运行函数。
特点:可传递,可运算。
1.1.4.2. 变量
可变化的量,变量是内存的标识,由变量名和变量值组成,每个变量都对应一块小内存;变量名用来查找对应的内存,变量值是内存中保存的数据;所有变量都有地址值,但只有对象的地址值才会被用到。
1.1.4.3. 内存
内存条通电后产生的可存储数据的空间,是临时的,断电后内存空间和存储的数据都消失。
内存分类
\qquad
栈空间: 全局变量、局部变量
\qquad
堆空间: 对象
1.1.4.4. tips
引用变量赋值
\qquad
n 个引用变量指向同一个对象,通过一个变量修改对象内部数据,其他所有变量看到的是修改之后的数据;
\qquad
两个引用变量指向同一个对象,让其中一个引用变量指向另一个对象,另一个依然指向之前的对象。
函数的参数传递
\qquad
a. 值传递,基本值或地址值;
\qquad
b. 值传递或引用传递(地址值)。
内存管理
\quad
内存生命周期
\qquad
a. 分配小内存空间,得到使用权;
\qquad
b. 存储数据,可以反复进行操作;
\qquad
c. 释放小内存空间。
\quad
释放内存
\qquad
局部变量:函数执行完自动释放;
\qquad
对象:先成为垃圾对象,之后由垃圾回收器进行回收。
1.2. 对象
1.2.1. 基础
什么是对象 多个数据的封装体,或者说,用来保存多个数据的容器;一个对象代表现实世界中的一个事物;
为什么要用对象 对象可以统一管理多个数据。
1.2.1.1. 对象的组成
属性 属性名 (字符串) +属性值 (任意类型);
方法 一种特别的属性,属性值是函数;
1.2.1.2. 访问对象的内部数据
对象.属性名
:编码简单,有时不能用,如:
var p = {}
// 1. 属性名包含特殊字符时,如‘-’、空格等;
p['content-type'] = 'text/json' // correct
p.content-type = 'text/json' // error
// 2. 变量名不确定;
var propName = 'myAge';
var value = 18;
p[propName] = value; // correct
p.propName = value; // error
console.log(p)
对象[‘属性名’]
:编码复杂,但是能通用。
1.3. 函数
什么是函数 实现特定功能的 n 条语句的封装体;只有函数是可以执行的,其他类型的数据不能执行。
为什么要用函数 提高代码复用;便于阅读交流;体现了封装的思想。
1.3.1. 定义函数
函数声明
function 函数名(参数){
各种语句;
}
表达式
var 变量名 = function(){
}
1.3.2. 调用函数
直接调用: test()
通过对象调用:obj.test()
new 调用:new test()
临时调用:test.call/apply(obj)
,临时让test()
成为obj
的方法,相当于 obj.test()
1.3.3. 回调函数
什么是回调函数 自己定义的函数,没有调用,但最终它执行了;
常见的回调函数 dom事件回调函数、定时器回调函数、ajax回调函数、生命周期回调函数
1.3.4. 立即执行函数表达式 IIFE (Immediately-Invoked Function Expression)
IIFE = 匿名函数自调用 (把完整的函数用小括号括起来然后调用)
作用 :隐藏实现;不会污染外部命名空间;用它编写 js 模块
此时函数在局部作用域里,不产生全局变量,只执行一次,执行完后就被回收
(function(){
var a = 1;
function test(){
console.log(++a);
}
window.$ = function(){
return {test: test};
} // 向外暴露一个全局函数
})();
$().test(); // 2
// window.$:把 window 对象传入这个匿名函数中,同时执行这个函数,在页面载入之前就执行
1.3.5. 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("yellow"); // this: new出来的Person对象 / p
p.getColor(); // this: p
var obj = {}; // this: obj
p.setColor.call(obj, "black"); // this: obj
var test = p.setColor;
test(); // this: Window
2. 函数
2.1. 原型与原型链
2.1.1. 原型
函数的 prototype 属性 每个函数都有一个 prototype 属性,它默认指向一个空的 Object 对象,称为原型对象;原型对象中有一个属性 constructor,指向函数对象。
给原型对象添加属性 令函数的所有实例对象自动拥有原型中的属性 (一般添加的都是方法)。
typeof 函数名.prototype
输出的是 'object'
function fun(){};
console.log(fun.prototype); // {}:默认指向一个 Object 空对象,里面没有我们的属性
console.log(fun.prototype.constructor === fun); // true,原型对象中的 constructor 属性指向函数对象
fun.prototype.test = function(){
console.log('test()');
};
console.log(fun.prototype); // { test: [Function (anonymous)] }:可以向里面添加属性
var newFun = new fun();
newFun.test(); // test():可以调用,添加成功
显式原型与隐式原型
每个 函数function
都有一个属性prototype
,即显式原型;每个 实例对象 都有一个属性__proto__
,即隐式原型。
函数的prototype
属性在定义函数时自动添加,默认值是一个空 Object 实例对象(Object
除外,Object.prototype = null
);
实例对象的__proto__
属性在创建对象时自动添加,默认值为其构造函数的 prototype 属性值。
可以直接操作显式原型,但不能直接操作隐式原型(ES6之前)。
// 定义构造函数
function Fun(){}; // 此时有一条内部语句 Fun.prototype = {}
console.log(Fun.prototype); // {}:默认指向一个 Object 空对象
var newFun = new Fun(); // 此时有一条内部语句 newFun.__proto__ = Fun.prototype
console.log(newFun.__proto__); // {}:指向 Fun() 的 prototype 值
console.log(newFun.__proto__ === Fun.prototype); // true
// 给原型添加方法
Fun.prototype.test = function(){
console.log("add property");
};
newFun.test(); // add property
2.1.2. 原型链 ( 隐式原型链 )
查找对象的属性( 方法 ) 访问一个对象的属性时,先在自身属性中查找,找到就返回;如果没找到,那么沿着__proto__
这条链向上查找,找到就返回;如果查到 Object 的原型对象(Object.prototype.__proto__ = null
)都没找到,那么返回 undefined。
function Fun()
相当于var Fun = new Function()
,即每一个函数都是Function
的实例,因此每一个函数都有一个__proto__
属性;由于Function = new Function()
,这导致了Function()
的显式原型和隐式原型指向同一个位置,即Function.__proto__ === Function.prototype
;最终,所有函数的__proto__
都是一样的。
构造函数的实例对象自动拥有构造函数原型对象的属性,利用的就是原型链。
function Fun(){
this.test1 = function(){
console.log("test1()");
};
};
Fun.prototype.test2 = function(){
console.log("test2()");
}
var newFun = new Fun();
console.log(newFun);
读取对象的属性值时,会自动到原型链中查找;设置对象的属性值时,不会查找原型链,如果当前对象中没有此属性,那么直接添加该属性并设置值;
方法一般定义在原型中,属性一般通过构造函数定义在对象本身上。
2.1.3. instanceof
语法 A instanceof B
规则 如果B
函数的显式原型对象在A
对象的原型链上,返回true
,否则返回false
。
function Fun(){};
var f1 = new Fun();
console.log(f1 instanceof Fun); // true
console.log(f1 instanceof Object); // true
console.log(Object instanceof Object); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function instanceof Function); // true
console.log(Object); // [Function: Object]
console.log(Function); // [Function: Function]
2.1.4. 小练习
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); // 1 undefined, 在 b 初始化完成后,A 的显示原型对象指向了一个新的对象,而b的隐式原型依然指向原来的位置。
console.log(c.n, c.m); // 2 3
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()
f.a(); // a()
f.b(); // undefined
2.2. 执行上下文与执行上下文栈
2.2.1. 变量提升与函数提升
变量声明提升 通过var
声明的变量,在定义语句之前就可以访问到(访问到的值是undefined
);
函数声明提升 通过function
声明的函数,在声明之前可以直接调用(其值是函数定义,是一个对象)。
fn(); // 函数声明提升,输出为 undefined
function fn(){
console.log(a); // 变量声明提升
var a = 4;
}
fn(); // error,遵循的是变量提升
var fn = function(){
console.log(a);
var a = 4;
}
2.2.2. 执行上下文
代码分类 全局代码、函数(局部)代码
上下文环境是动态的,调用函数时创建,函数调用结束后上下文环境被释放。
全局执行上下文
- 在全局作用域确定之后,执行全局代码前将
window
确定为全局执行上下文; - 对全局数据进行预处理:
var
定义的全局变量 ⇒ \Rightarrow ⇒undefined
,添加为window
的属性;
function
声明的全局函数 ⇒ \Rightarrow ⇒ 赋值(fun
),添加为window
的方法;
this
⇒ \Rightarrow ⇒ 赋值为window
。
函数执行上下文
- 在调用函数、准备执行函数体之前,创建对应的函数执行上下文对象;
- 对局部数据进行预处理:
形参变量 ⇒ \Rightarrow ⇒ 赋值(实参) ⇒ \Rightarrow ⇒ 添加为执行上下文的属性;
arguments
⇒ \Rightarrow ⇒ 赋值(实参列表),添加为执行上下文的属性;
var
定义的局部变量 ⇒ \Rightarrow ⇒undefined
,添加为执行上下文的属性;
function
声明的函数 ⇒ \Rightarrow ⇒ 赋值(fun
),添加为执行上下文的方法;
this
⇒ \Rightarrow ⇒ 赋值(调用函数的对象); - 开始执行函数体代码。
2.2.3. 执行上下文栈
- 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象;
- 在全局执行上下文(
window
)确定后,将其添加到栈中(压栈); - 在函数执行上下文创建后,将其添加到栈中(压栈);
- 在当前函数执行完后,移除栈顶对象(出栈);
- 当所有代码执行完后,栈中只剩下
window
。
// 1. 进入全局执行上下文,确定 window
console.log(window);
var a = 10;
var bar = function(x){
var b = 5;
foo(x + b); // 3. 进入foo执行上下文
};
var foo = function(y){
var c = 5;
console.log(a+c+y);
};
bar(10); // 2. 进入bar执行上下文
2.2.4. 练习
先执行变量提升,再执行函数提升
function a(){};
var a;
console.log(typeof a); // function
用var
声明了b
,因此b
进行了变量提升,成为window
的属性,使得b in window
返回值为true
,不进入if
语句,b
不能被赋值,输出为undefined
。
if(!(b in window)){
var b = 1;
}
console.log(b); // undefined
函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但会被变量赋值后覆盖。 首先进行变量提升,c = unefined
,然后进行函数提升,将函数赋值给c
,接着c
被赋值为1,覆盖了函数。
var c = 1;
function c(c){
console.log(c);
};
c(2);
// c(2);
// ^
// TypeError: c is not a function
2.3. 作用域与作用域链
2.3.1. 作用域
一块“地盘”,一个代码段所在的区域;它是静态的(相对于上下文对象),在编写代码时确定,一直存在且不会再变化。
2.3.1.1. 分类
全局作用域、局部作用域、块作用域(ES6之后才有)。
ES5中只有全局作用域和局部作用域,因此全局作用域中只有函数具有切割作用域的功能,如果函数外面没有另一层函数,那就直接看到全局作用域。
2.3.1.2. 作用
隔离变量,不同作用于下同名变量不会有冲突。
全局作用域只有一个;有几个函数,就有几个局部作用域。
2.3.1.3. 作用域与上下文
执行上下文环境(对象)从属于所在的作用域。全局上下文环境 ⇒ \Rightarrow ⇒ 全局作用域;函数上下文环境 ⇒ \Rightarrow ⇒ 对应的函数使用域。
2.3.2. 作用域链
多个上下级关系的作用域形成的链,它的方向是从下向上的(从内向外),查找变量时沿着作用域链来查找。
查找规则
- 在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进行下一步;
- 在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进行下一步;
- 再次执行上一步的相同操作,直至全局作用域,如果仍然找不到,就抛出异常。
2.3.3. 练习
var x = 10;
function fn(){
console.log(x);
};
function show(f){
var x = 20;
f();
}
show(fn); // 10
作用域在函数定义时已经确定,此时fn()
的作用域中没有x
,因此去全局作用域中找x
,也就是说调用fn()
时用到的x
只能是值为10
的x
。
var fn = function(){
console.log(fn);
};
fn(); // [Function: fn]
var obj = {
fn2: function(){
console.log(fn2); // ReferenceError: fn2 is not defined
console.log(this.fn2); // [Function: fn2]
}
};
obj.fn2();
在function(){console.log(fn2);}
的作用域中,没有fn2
,因此去上一级(全局作用域)中寻找fn2
,仍然没有。注意:obj
中的fn2
只是obj
的一个属性,并不存在于全局作用域中。
2.4. 闭包
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数) 时,就产生了闭包。闭包存在于嵌套的内部函数中,可以认为闭包就是能够读取其他函数内部变量的函数。函数内部的变量会一直存在于内存中,不会被立即释放。
产生闭包的条件 存在函数嵌套;内部函数引用了外部函数的数据;执行了内部函数的定义。
2.4.1. 常见的闭包
- 将函数作为另一个函数的返回值
function fn1(){
var a = 2;
function fn2(){
a++;
console.log(a);
};
return fn2;
};
var f1 = fn1();
f1(); // 3
f1(); // 4
var f2 = fn1();
f2(); // 3
f2(); // 4
闭包一直存在导致a
的值一直累加;调用几次外部函数,就产生几个闭包。
- 将函数作为实参传递给另一个函数调用
function showDelay(msg, time){
setTimeout(function(){
alert(msg);
}, time)
};
showDelay('jiumi', 2000);
2.4.2. 闭包的作用
- 延长局部变量的生命周期
使函数内部的变量在函数执行完后仍然存活在内存中; - 在函数外部也可以操作(读 / 写)函数内部的数据(变量 / 函数)
2.4.3. 闭包的生命周期
- 在嵌套的内部函数定义执行完时产生(不需要调用)
- 在嵌套的内部函数成为垃圾对象时死亡
2.4.4. 闭包的应用 - 自定义JS模块
- JS模块
具有特定功能的js文件;
将所有的数据和功能都封装到函数内部,它们都是私有的;
只向外暴露一个包含 n 个方法的对象或函数;
模块的使用者只需要通过模块暴露的对象调用方法来实现对应的功能。
function yyt(){
var msg = 'The Most powerful yyt';
function msgUpper(){
console.log('msgUpper: '+msg.toUpperCase());
};
function msgLower(){
console.log('msgLower: '+msg.toLowerCase());
};
// 用一个对象封装想要暴露的方法
return {up:msgUpper, low:msgLower};
};
var fn = yyt();
fn.up(); // msgUpper: THE MOST POWERFUL YYT
fn.low(); // msgLower: the most powerful yyt
2.4.5. 闭包的缺点
- 函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
- 容易造成内存泄露
⇒ \Rightarrow ⇒ 能不用闭包就不用,并且要及时释放。
2.4.6. 内存溢出与内存泄露
- 内存溢出
一种程序运行出现的错误,当程序运行需要的内存超过了剩余内存时抛出该错误。
var obj = {};
for(var i = 0; i < 10000; ++i){
obj[i] = new Array(10000000);
console.log('-----');
}
- 内存泄露
占用的内存没有及时释放;
内存泄露积累多了就容易导致内存溢出;
常见的内存泄露情况:意外的全局变量、没有及时清理的计时器或回调函数;闭包;
// 1. 意外的全局变量
function(){
a = 3; // 不声明就直接赋值相当于 window.a
console.log(a);
}
fn();
// 2. 没有及时清理的计时器或回调函数
setInterval(function(){console.log('-----');}, 1000) // 启动循环定时器后不清理,导致它一直运行
// 3. 闭包
function fn1(){
var a = 4;
function fn2(){
console.log(++a);
};
return fn2;
}
var f = fn1();
f(); // 调用后没有释放,闭包一直存在
2.4.6. 练习
var name = "The Window";
var obj = {
name: "My Object",
getNameFunc: function(){
return function(){ return this.name; };
}
};
alert(obj.getNameFunc()()); // The Window
this
指向调用了内层函数的那个对象,此时内部函数并不是通过调用执行的,而是在obj.getNameFunc()
这个函数后面加了()
使其立即执行,因此this
指向的是window
。
var name = "The Window";
var obj = {
name: "My Object",
getNameFunc: function(){
var that = this;
return function(){ return that.name; };
}
};
alert(obj.getNameFunc()()); // My Object
this
指向调用了外层函数的那个对象,此时是obj
通过方法调用了外层函数,因此this
指向的是obj
。
function fun(n, o){
console.log(o);
return {
fun: function(m){
return fun(m, n);
}
};
};
var a = fun(0); // o 没有值,输出 undefined并返回一个对象,对象中有一个属性fun,其值是一个匿名函数,并产生闭包,闭包中 n = 0;
a.fun(1); // return fun(1, 0), 输出 0,并产生一个新的闭包,闭包中 n = 1,但由于没有变量调用它,因此闭包马上消失
a.fun(2); // return fun(2, 0), 输出0,同上
a.fun(3); // return fun(3, 0), 输出0,同上
var b = fun(0).fun(1).fun(2).fun(3);
// o 没有值,输出 undefined并返回一个对象,对象中有一个属性fun,其值是一个匿名函数,并产生闭包,闭包中 n = 0; return fun(1, 0),输出0,并产生一个新的闭包,闭包中 n = 1; 新的闭包被调用,return fun(2, 1), 输出1,并产生一个新的闭包,闭包中 n = 2; return fun(2,3),输出2,并产生一个新的闭包,闭包中 n = 2,闭包被赋值给 b,没有消失
var c = fun(0).fun(1); // o 没有值,输出 undefined;
c.fun(2); // ,1
c.fun(3); // ,1
递!归!地!狱!
3. 对象
3.1. 对象创建模式
3.1.1. Object构造函数模式
先创建空的Object对象,然后动态添加属性或方法;
适用于起始时不确定对象内部数据的情况;
问题在于语句太多。
var p = new Object();
p.name = 'yyt';
p.age = 16;
p.setName = function(name){
this.name = name;
};
console.log(p); // { name: 'yyt', age: 16, setName: [Function (anonymous)] }
3.1.2. 对象字面量
使用{}
创建对象,同时指定属性和方法;
适用于起始时不确定对象内部数据的情况;
问题在于语句太多。
var p = {
name : 'yyt',
age : 16,
setName : function(name){
this.name = name;
}
};
console.log(p); // { name: 'yyt', age: 16, setName: [Function: setName] }
p.setName('lyl');
console.log(p); // { name: 'lyl', age: 16, setName: [Function: setName] }
3.1.3. 工厂模式
通过工厂函数动态创建对象并返回;
适用于需要创建多个对象的场景;
问题在于返回的对象没有一个具体的类型,都是Object
类型
⇒
\Rightarrow
⇒ 在有多个工厂函数的情况下,无法区分哪个对象是哪个函数的实例;
工厂函数 返回一个对象的函数
function createPerson(name, age){
var obj = {
name : name,
age : age,
setName : function(name){
this.name = name;
},
setAge : function(age){
this.age = age;
},
};
return obj;
};
var p = createPerson('yyt', 12);
console.log(p);
/* {
name: 'yyt',
age: 12,
setName: [Function: setName],
setAge: [Function: setAge]
}*/
3.1.4. 自定义构造函数模式
自定义构造函数,通过new创建对象;
适用于需要创建多个确定类型对象的场景;
问题在于每个对象都有相同的数据,浪费内存;
function Person(name, age){
this.name = name;
this.age = age;
this.setName = function(name){
this.name = name;
};
this.setAge = function(age){
this.age = age;
};
};
function Cat(name, age){
this.name = name;
this.age = age;
this.setName = function(name){
this.name = name;
};
this.setAge = function(age){
this.age = age;
};
};
var p = new Person('yyt', 12);
var c = new Cat('dudu', 3);
console.log(p); // Person { name: 'yyt', age: 12 }
console.log(c); // Cat { name: 'dudu', age: 3 }
3.1.5. 构造函数+原型的组合模式
自定义构造函数,属性在函数中初始化,方法添加到原型中以节省内存;
适用于需要创建多个确定类型对象的场景;
function Person(name, age){
this.name = name;
this.age = age;
};
Person.prototype.setName = function(name){
this.name = name;
};
Person.prototype.setAge = function(age){
this.age = age;
};
function Cat(name, age){
this.name = name;
this.age = age;
};
Cat.prototype.setName = function(name){
this.name = name;
};
Cat.prototype.setAge = function(age){
this.age = age;
};
var p = new Person('yyt', 12);
var c = new Cat('dudu', 3);
console.log(p); // Person { name: 'yyt', age: 12 }
console.log(c); // Cat { name: 'dudu', age: 3 }
3.2. 继承模式
3.2.1. 原型链继承
- 定义父类型构造函数;
- 给父类型的原型添加方法;
- 定义子类型的构造函数;
- 创建父类型的实例对象,赋值给子类型的原型;
- 将子类型原型的构造属性设置为子类型;
- 给子类型的原型添加方法;
- 创建子类型的对象,可以调用父类型的方法。
关键: 子类型的原型为父类型的一个实例对象。
function Father(){ // 1. 定义父类型构造函数;
this.faProp = 'father';
};
Father.prototype.showFatherProp = function(){ // 2. 给父类型的原型添加方法;
console.log(this.faProp);
};
function Son(){ // 3. 定义子类型的构造函数;
this.sonProp = 'son';
};
Son.prototype = new Father(); // 4. 创建父类型的实例对象,赋值给子类型的原型;
Son.prototype.constructor = Son; // 5. 将子类型原型的构造属性设置为子类型;
Son.prototype.showSonProp = function(){ // 6. 给子类型的原型添加方法;
console.log(this.sonProp);
};
var sub = new Son(); // 7. 创建子类型的对象,可以调用父类型的方法。
sub.showFatherProp(); // father
sub.showSonProp(); // son
console.log(sub); // Son { sonProp: 'son' }
3.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.price = price;
};
var s = new Student('yyt', 16, 8000);
console.log(s.name, s.age, s.price); // yyt 16 8000
3.2.3. 组合继承(重点嗷 构造函数 + 原型链继承)
function Person(name, age){
this.name = name;
this.age = age;
};
Person.prototype.setName = function(name){
this.name = name;
};
Person.prototype.setAge = function(age){
this.age = age;
};
function Student(name, age, price){
Person.call(this, name, age); // 相当于 this.Person(name, age)
this.price = price;
};
Student.prototype = new Person(); // 为了能看到父类型的方法
Student.prototype.constructor = Student;
Student.prototype.setPrice = function(price){
this.price = price;
};
var s = new Student('yyt', 16, 8000);
console.log(s.name, s.age, s.price); // yyt 16 8000
s.setName('lyl');
s.setAge(18);
s.setPrice(16000);
console.log(s.name, s.age, s.price); // lyl 18 16000
4. 线程机制与事件机制
4.1. 进程与线程
4.1.1. 进程 Process
程序的依次运行,占有一片独有的内存空间。可以通过Windows任务管理器查看进程。
4.1.2. 线程 thread
进程内一个独立的执行单元,是程序执行的一个完整流程,是CPU最小的调度单元。
4.1.3. others
- 应用程序必须运行在某个进程的某个线程上;
- 一个进程中至少有一个运行的线程,称为主线程,在进程驱动后自动创建;
- 一个进程中可以同时运行多个线程,此时程序是多线程运行的;
- 一个进程内的数据可以供其中的多个线程直接共享;
- 多个进程之间的数据是不能直接共享的;
- 线程池(thread pool):保存多个线程对象的容器,实现线程对象的反复利用;减少了进程创建销毁线程的开销。
- 多进程与多线程
多进程: 一个应用程序可以同时启动多个实例运行
多线程: 在一个进程内,同时有多个线程运行 - 单线程与多线程
单线程: 顺序编程简单易懂;但是效率低。
多线程: 能有效提高CPU的利用率;但是创建多线程和进行线程间切换都需要开销,并且有死锁与状态同步问题。 - JS的线程
JS是单线程运行的,但是使用 H5 的Web Workers 可以多线程运行。 - 浏览器的线程
多线程嗷。 - 浏览器的进程
单进程:Firefox、老版IE
多进程:Chrome、新版IE
可以查看浏览器是否是多进程运行。
4.2. 浏览器内核
- 内核是支撑浏览器运行的最核心的程序,不同的浏览器可能不一样
Chrome、Safari:webkit
Firefox:Gecko
360、搜狗等国内浏览器:Trident + webkit - 内核由很多模块组成
主线程
js引擎模块:负责js程序的编译与运行
html、css文档解析模块:负责页面文本的解析
DOM/CSS模块:负责DOM/CSS在内存中的相关处理
布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)
……
分线程
定时器模块:负责定时器的管理
事件响应模块:负责事件的管理
网络请求模块:负责ajax请求
4.3. 关于定时器
定时器并不能保证真正定时执行,一般会延迟一丁点,是可以接受的。但也有可能延迟很长时间,不能接受。
定时器的回调函数在主线程执行,因为js是单线程的。
定时器由事件循环模型实现。
4.4. JS是单线程执行的
4.4.1. 证明
setTimeout()
的回调函数是在主线程执行的;
定时器的回调函数只有当运行栈中的代码全部执行完后才有可能执行。
setTimeout(function(){console.log("timeout 2");}, 2000);
setTimeout(function(){console.log("timeout 1"); alert('timeout 1');}, 1000);
setTimeout(function(){console.log("timeout 0");}, 0);
function fn(){
console.log("fn()");
};
fn();
console.log("before alert");
alert('-----'); // 暂停当前主线程的执行,也暂停了计时器,点击确定后才恢复
console.log("after alert");
/*
fn()
before alert
after alert
timeout 0
timeout 1 // 点击确定后,又过了1秒才输出 timeout 2
timeout 2
*/
4.4.2. 原因
JS的单线程与它的用途有关。作为浏览器的脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
4.4.3. 代码分类
初始化代码
回调代码
4.4.4. js引擎的流程
- 执行初始化代码(同步代码),包含一些特别的代码:设置定时器、绑定事件监听、发送ajax请求;
- 在后面的某个时刻执行回调代码(异步代码),处理回调逻辑。
4.5. 浏览器的事件循环(轮询)模型
- 组成部分
事件管理模块:定时器管理模块、DOM管理模块
回调队列 - 运转流程
1.执行初始化代码,将事件回调函数交给对应模块管理;
2.当事件发生时,管理模块将回调函数及其数据添加到回调队列中;
3.只有当初始化代码执行完后(可能需要一定的时间),才会遍历读取回调队列中的回调函数并执行。
- 相关概念
执行栈:execution stack,所有的代码都在此空间中执行;
浏览器内核:browser core;
任务队列 / 消息队列 / 事件队列:都是回调队列 callback queue;
事件轮询:event loop, 执行完初始化代码之后,从任务队列中循环取出回调函数,放入执行栈中处理(一个接一个);
事件驱动模型:event-driven interaction model;
请求响应模型:request-response model,浏览器发送请求给服务器,服务器接受请求、处理请求,返回一个响应数据,浏览器接收响应数据,进行后续操作。
4.6. H5 Web Workers(多线程)
4.6.1. 介绍
- H5规范提供了js分线程的实现,称为 Web Workers。可以将一些大计算量的代码交由 Web Workers 运行而不冻结用户界面。但是子线程完全受主线程控制,并且不能操作DOM,因此这个新标准并没有改变JS单线程的本质。
- 相关API
Worker
:构造函数,加载分线程执行的 js 文件;
Worker.prototype.onmessage
:接收另一个线程的回调函数;
Worker.prototype.postMessage
:向另一个线程发送消息。 - 不足
Worker 内代码不能操作 DOM:分线程的全局对象不再是window,所以在分线程中不能更新界面;
不能跨域加载 JS;
不是每个浏览器都支持这个新特性。
4.6.2. 使用
1. 创建在分线程执行的 js 文件;
2. 在主线程的 js 中发消息并设置回调。
实现斐波那契数列
1. worker.js
function fibonacci(n){
if(n <= 2){
return 1;
}
else{
return fibonacci(n-1)+fibonacci(n-2);
}
};
var onmessage = function(event){
var number = event.data;
console.log('分线程接收到主线程的数据:'+number);
// 计算
var result = fibonacci(number);
// 向主线程发送消息
postMessage(result);
console.log('分线程向主线程返回数据:'+result);
};
2. learn.js
var input = document.getElementById('idx');
var btn = document.getElementById('btn');
btn.onclick = function(){
var n = input.value;
// 创建一个 Worker 对象
var worker = new Worker('worker.js');
// 绑定回调代码,当收到消息时触发
worker.onmessage = function(event){
alert(event.data);
};
// 向分线程发送消息
worker.postMessage(n);
};
<input type="text" placeholder="数值" id="idx">
<button id="btn">计算</button>