文章目录
数据类型
基本数据类型
- null
- undefined
- number
- string
- boolean
引用数据类型
- object
- array
- function
- date
- symbol(ES6引入)
检测数据类型的方法
typeof
typeof [1,2] // objectinstanceof
[1,2] instanceof Array // true
函数
声明式定义函数
表达式定义函数(匿名函数)
箭头函数(es6)
特性:
- 箭头函数没有自己的
this对象,this指向创建时所在执行上下文中的this; - 不可以当作构造函数;
- 不可以使用
arguments对象 - 不可用使用
yield命令
this指向
call、apply、bind
变量、作用域
变量定义
var
存在变量提升、暂时性死区
let、const(es6)
引入了块级作用域
作用域
作用域是全局执行上下文和局部执行上下文中的变量对象(VO)/活动对象(AO)
作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链,用来保证对执行环境有权访问的变量和函数的有序访问。
原型、原型链
构造函数、原型和实例的关系
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
原型链的基本概念
让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型对象的指针,而另一个原型中也包含着指向另一个构造函数的的指针。假如另一个原型又是另一个原型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
原型链继承
// 原型链继承
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
// 继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
var instance= new SubType();
console.log(instance.getSuperValue()); // true
原型链继承的问题
- 当 SuperType 中存在一个引用类型的属性时,每一个SuperType的实例都会有各自的引用属性,但是SubType 使用原型链继承了 SuperType,此时SubType的原型是SuperType的实例,这个时候再创建多个SubType的实例,这些实例就只会共享一个引用属性,因此只要一个实例中改变了引用属性中的值之后,其他实例里面的值都会改变。
- 在创建子类型的实例时,不能向超类型的构造函数传递参数。
借用构造函数
在子类构造函数的内部调用超类型的构造函数。
function SuperType(){
this.colors = ['red', 'blue', 'green'];
}
function SubType(){
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors);
var instance2 = new SubType(); // ['red', 'blue', 'green', 'black']
console.log(instance2.colors); // ['red', 'blue', 'green']
组合继承
组合继承,有时候也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
function SuperType(name){
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
instance1.sayName(); // 'Nicholas'
instance1.sayAge(); // 29
var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // ['red', 'blue', 'green']
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 27
缺点:无论在什么情况下,都会调用两次超类型构造函数。
原型式继承
使用 Object.create() 规范化了原型式继承,这个方法接收两个参数:
- 一个用作新对象原型的对象
- 一个为新对象定义额外属性的对象。
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var anotherPerson = Object.create(person, {
name: {
value: 'Greg'
}
});
console.log(anotherPerson.name) // 'Greg'
寄生式继承
寄生式继承是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
function createAnother(origin) {
var clone = object(origin);
clone.sayHi = function(){
console.log('Hi');
}
return clone;
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'Hi'
其中使用的 object 函数并不是必须的,任何能够返回新对象的函数都适用于此模式。
寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype上创建不必要、多余的属性。于此同时,原型链还能保持不变。
闭包
闭包就是能够读取其它函数内部变量的函数
使用方法:在一个函数内部创建另一个函数
最大用处有两个:读取其他函数的变量值,让这些变量始终保存在内存中
缺点:会引起内存泄漏(引用无法被销毁,一直存在)
事件、事件流
事件捕获
不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。
事件冒泡
IE的事件流叫做事件冒泡,即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点。
DOM2级事件流
三个阶段:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
js事件循环机制
垃圾回收机制
标记清楚算法
大致过程
- 垃圾收集器在运行时会为内存中的所有变量都添加上一个标记
- 从各个对象的根节点开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把内存中的对象都标记为0,等待下一轮回收
优点
实现简单,打标记只分为打与不打,所以用一位二进制就可以标记
缺点
在清除之后,剩余对象的内存位置是不会改变的,就会导致空闲的内存空间不连续,出现内存碎片。
为找到合适的内存块,可以采用三种分配策略
First-fit,找到大于等于size的块立即返回Best-fit,遍历整个空闲列表,返回大于等于size的最小分块Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size大小,并将该部分返回
综上标记清除算法有两个明显的缺点:
- 内存碎片化:空闲内存不连续,容易出现很多空闲的内存块,还可能出现分配的内存过大而找不到合适的块。
- 分配速度慢:即使使用
First-fit策略,但其操作仍然是一个O(n)的操作。
标记整理算法
在标记结束后,会将活着的对象往内存的一段移动,最后清理掉边界的内存
引用计数算法
它的策略就是跟踪记录每个值被使用得次数
- 当声明了一个变量并且将一个引用类型的值赋给这个变量时,这个值的引用次数就为1
- 同一个值又被赋给另一个变量,那么值的引用数加1
- 当该变量的值被其他值覆盖,那么被覆盖值得引用次数就减1
- 当这个值得引用次数为0时,说明这个值没有变量在使用了,就回收空间。
优点
- 引用计数在值的引用次数为0的时候就被回收,所以它可以立即回收垃圾
- 标记清除算法需要每隔一段时间进行一次,并且每次都需要暂停主线程其执行一段时间的
GC,另外,标记清楚算法需要遍历堆里面的活动和非活动对象来清除,而引用计数则只需要在引用时计数就可以了。
缺点
- 引用计数算法需要一个计数器,而计数器需要占很大的空间,因为引用数量的上限不知道
- 无法解决循环引用造成的内存无法回收问题
V8对GC的优化
分代式垃圾回收
新老生代
- 新生代:存活时间较短,通常支持1~8M的容量
- 老生代:存活时间较长,容量通常较大
新生代垃圾回收
通过一个叫 Scavenge的短发进行垃圾回收,在该算法中,主要采用了一种叫 Cheney 算法。
Cheney 算法将堆内存分为两部分,空闲区和使用区
新加入的对象会被存放到使用区,当使用区满了之后会执行一次垃圾清理操作。
在进行垃圾回收时,会对使用区的活动对象进行标记,然后将标记完之后的活动对象复制到空闲区,然后将使用区清空,最后进行角色互换,即原来的使用区变成了空闲区,原来的空闲区变成了使用区。
当一个对象经过多次垃圾清理之后依然存活,它就会被认为是生命周期较长的对象,随后会被移动到老生代当中,采用老生代回收策略进行回收。
还有一种情况是,如果复制一个对象到空闲区时,空闲区的空间占用超过了25%,那么这个对象会被直接晋升到老生代空间当中,之所以是25%,是因为如果占比过大,将会影响后续内存分配。
老生代垃圾回收
老生代中的对象通常比较大,如果像新生代中的对象一样复制来复制去会非常耗时,从而导致回收执行效率不高,所以老生代回收器就直接采用上文说到的标记清除算法。
并行回收
由于js是运行在主线程上的一门单线程语言,所以在进行垃圾回收时,就会阻塞js脚本的执行,需等待垃圾回收完毕之后再恢复脚本的执行,这种行为称为全停顿。
如果垃圾回收时间过长,那么可能就会造成页面卡顿等问题。因此V8团队引入了并行回收机制。
所谓并行,也就是同时的意思,它指垃圾回收器在主线程上运行的同时,开启多个辅助线程,同时执行同样的回收工作。
增量标记与懒性清理
虽然引入并行回收策略提高了垃圾回收效率,但其实它还是以一个全停顿的回收方式,对于老生代来说,它内部存放的都是一些比较大的对象,对于这些大的对象,即使使用并行策略,但是还是会消耗大量的时间。
所以,2011年,V8对老生代的标记进行了优化,从全停顿切换到了增量标记。
什么是增量?
增量就是将一次GC分成很多小步,每执行完一个小步就让应用逻辑执行一会儿,这样交替多次后完成一次GC。
但是在每一小步完成之后如何暂停下来去执行应用程序,然后又怎么恢复?应用程序又改变了标记好的引用关系该怎么办?
为解决这两个问题,V8采用了三色标记和写屏障
三色标记法
三色标记法使用每个对象两个标记位和一个标记工作表实现,两位标记位编码三种颜色:白,灰,黑
- 白色指自身未被标记的对象
- 灰色指自身被标记,成员变量(该对象的引用对象)未被标记
- 黑色指自身和成员变量都被标记
起初所有对象都是白色,从根节点开始,根节点被标记为灰色并推入到标记工作表当中,当回收器从标记工作表弹出对象,并访问该对象的引用对象时,将由灰色变成黑色,同时将引用对象转成灰色并加入标记工作表里面。
就这样一直往下走,直到没有可标记灰色的对象时,也就是无可达(无引用到)的对象了,那么剩下的所有白色对象都是无法到达的,即等待回收
我的总结:My Summary
参考资料