2021秋招复习——Javascript

数据类型

基本数据类型

  1. null
  2. undefined
  3. number
  4. string
  5. boolean

引用数据类型

  1. object
    • array
    • function
    • date
  2. symbol(ES6引入)

检测数据类型的方法

  1. typeof

    typeof [1,2]  // object
    
  2. instanceof

    [1,2] instanceof Array  // true
    

函数

声明式定义函数

表达式定义函数(匿名函数)

箭头函数(es6)

特性:

  1. 箭头函数没有自己的this对象,this指向创建时所在执行上下文中的this;
  2. 不可以当作构造函数;
  3. 不可以使用 arguments对象
  4. 不可用使用 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

原型链继承的问题

  1. 当 SuperType 中存在一个引用类型的属性时,每一个SuperType的实例都会有各自的引用属性,但是SubType 使用原型链继承了 SuperType,此时SubType的原型是SuperType的实例,这个时候再创建多个SubType的实例,这些实例就只会共享一个引用属性,因此只要一个实例中改变了引用属性中的值之后,其他实例里面的值都会改变。
  2. 在创建子类型的实例时,不能向超类型的构造函数传递参数。

借用构造函数

在子类构造函数的内部调用超类型的构造函数。

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() 规范化了原型式继承,这个方法接收两个参数:

  1. 一个用作新对象原型的对象
  2. 一个为新对象定义额外属性的对象。
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级事件流

三个阶段:

  1. 事件捕获阶段
  2. 处于目标阶段
  3. 事件冒泡阶段

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
参考资料

「硬核JS」你真的了解垃圾回收机制吗


版权声明:本文为qq_43717065原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。