javascript一些知识点总结

Javascript基础

变量和类型

面向对象

作用域和闭包

Node.js

ES6相关

前端安全

  1. XSS攻击的原理、分类、具体案例,如何防御
  2. CSRF攻击的原理、具体案例,如何防御
  3. HTTP劫持、页面劫持的原理,如何防御

javascript数据类型

ECMAScript 标准定义了 7 种数据类型:

6种原始类型-基本数据类型(按值访问)

  • Null (js中的数据在底层是以二进制存储,如果前三位为0,那么就会判定为object,而null的所有都为0)
  • Undefined
  • 基本包装类型(自动创建的基本包装类型的对象—非Boolean,Number, String内置函数new出来的,对象只存代码的执行瞬间)
    • Number(基于 IEEE 754 标准的双精度 64 位二进制格式的值——数字、±Infinity、NaN)
    • String
    • Boolean
  • Symbol (ECMAScript 6新定义,实例是唯一且不可改变的)

引用类型: Object(包括Object/Array/Function/RegExp/Date)

弱类型语言

  • 变量的类型就是其值的类型,也就是说变量当前的类型由其值所决定
  • 变量类型可以改变,a = 1 类型是number, a= “hello”, 类型变为 string

类型检测的方式

  1. typeof;不能检测引用类型;
  2. instanceof;不能检测基本类型;只能判断是否是当前类型实例;不能判断到底属于哪种类型;
  3. Object.prototype.toString.call();推荐使用;
  4. constructor;易被修改,不能跨iframe;

优缺点

不同类型的优缺点 typeof instanceof constructor Object.prototype.toString.call
优点 使用简单 能检测出引用类型 基本能检测所有的类型(除了null和undefined) 检测出所有的类型
缺点 只能检测出基本类型(除了null) 不能检测出基本类型,且不能跨iframe constructor易被修改,也不能跨iframe IE6下,undefined和null均为Object

如何准确的判断数组类型

  • a instanceof Array => instanceof和constructor不能跨iframe,所以此方案不行!
  • Object.prototype.toString.call(a) === ‘[object Array]’ => 应选方案

数组相关的常用方法

push/pop, shift/unshift, split/join, slice/splice/concat, sort/reverse, map/reduce, forEach, filter

slice: slice是指定在一个数组中的元素创建一个新的数组,即原数组不会变

var color = new Array('red','blue','yellow','black');
var color2 = color.slice(1,2);
alert(color);   //输出   red,blue,yellow,black
alert(color2);   //输出   blue;注意:这里只有第二项一个值

splice: splice是JS中数组功能最强大的方法,它能够实现对数组元素的删除、插入、替换操作,返回值为被操作的值

var color = new Array('red','blue','yellow','black');

  • splice删除:  color.splice(1,2) (删除color中的1、2两项);
  • splice插入:  color.splice(1,0,‘brown’,‘pink’) (在color键值为1的元素前插入两个值);
  • splice替换:  color.splice(1,2,‘brown’,‘pink’) (在color中替换1、2元素);

字符串相关的常用方法

indexOf/lastIndexOf/charAt, split/match/test, slice/substring/substr, toLowerCase/toUpperCase

对象的底层数据结构

js一切皆对象,所以,js的一些引用类型是特殊封装的对象

Object底层实现

Object => HeapObject => JSReceiver => JSObject

JS Object类图

  • V8里面所有的数据类型的根父类都是Object
  • Object派生HeapObject,提供存储基本功能
  • 往下的JSReceiver用于原型查找
  • 再往下的JSObject就是JS里面的Object
  • Array/Function/Date等继承于JSObject
  • 左边的FixedArray是实际存储数据的地方

Array底层实现

Object => HeapObject => JSReceiver => JSArray // 看V8的源码

array 是在 object 的基础上继续封装而实现的,

动态数组,动态分配内存,跟java里的ArrayList, C++里的vector比较类似

  • push扩容:原数组长度的1.5倍+16
  • pop减容:容量大于等于length的2倍,容量减为数组长度

Map

map 和 set

Object => HeapObject => JSReceiver => JSCollection

symbol类型在实际开发中的应用

  1. 定义不需要对外操作和访问的属性
  2. 替代常量;不需要担心常量名字重复
  3. 定义类的私有属性/方法

不需要对外操作和访问的属性使用Symbol来定义

  • Object.keys()或者for...in不能枚举 Symbol 属性
  • JSON.stringify()将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外:

使用Symbol来替代常量

const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()

替换成:

const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()

好处:不会重复!

使用Symbol定义类的私有属性/方法

const PASSWORD = Symbol()

class Login {
  constructor(username, password) {
    this.username = username
    this[PASSWORD] = password
  }

  checkPassword(pwd) {
      return this[PASSWORD] === pwd
  }
}

export default Login

注册和获取全局Symbol: Symbol.for()

let gs1 = Symbol.for('global_symbol_1')  //注册一个全局Symbol
let gs2 = Symbol.for('global_symbol_1')  //获取全局Symbol

gs1 === gs2  // true

变量在内存中的存储

基本数据类型在 栈中;对象在堆中,对象的引用在栈中

堆和栈的区别

  • 栈:基本数据类型和引用,值访问,存储的值大小固定,系统自动分配内存空间;空间小,运行效率高;后进先出;
  • 堆:存储引用的数据,按引用访问,存储的值大小不定,可动态调节,代码指定分配,空间大,运行效率低,无序存储

装箱拆箱操作

  • 装箱:把基本数据类型转化为对应的引用数据类型的操作;基本类型值=>对象,js内部实现;
  • 拆箱:将引用类型对象转换为对应的值类型对象:通过引用类型的valueOf()或者toString()方法来实现

null和undefined的区别

  • null表示没有对象,即该处不应该有值
  • undefined表示缺少值,即此处应该有值,但没有定义

null和undefined转换成number数据类型时:

  • null 默认转成 0
  • undefined 默认转成 NaN

隐式类型转换

可能发生隐式类型转换的场景以及转换原则

弱类型语言,会把变量隐式转换成自己需要的类型

  • 自动转换 Boolean
    • if 语句 或者其他需要 Boolean 的地方
    • == 两个等号判断
  • 运算符
    • 在非 Numeber 类型进行数学运算符 - * / 时,会先将非 Number 转换成 Number 类型。
    • 运算符要考虑字符串的情况,在操作数中存在字符串时,优先转换成字符串,

应如何避免或巧妙应用

避免:

  • 先进行显示类型转换再应用
  • 判断相等时使用 === 而不是 ==

谈谈你对面向对象是如何理解的

面向对象的底层其实还是面向过程,把面向过程抽象成,然后封装

面向对象的三大特性:

  1. 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
  2. 继承:提高代码复用性;继承是多态的前提。
  3. 多态:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。

面向过程和面向对象对比

面向过程:

  • 优点:性能比面向对象好,因为类调用时需要实例化,开销比较大,比较消耗资源。
  • 缺点:不易维护、不易复用、不易扩展.

面向对象:

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
  • 缺点:性能比面向过程差

常用js类定义的方法有哪些

两种方式:构造函数+原型链, 对象创建

  • 构造函数+原型链

    function Person(){
    	this.name = 'michaelqin';
    }
    Person.prototype.sayName = function(){
    	console.log(this.name);
    }
    
    var person = new Person();
    person.sayName();
    
  • 对象创建 Object.create({某某对象});

    var Person = {
    	name: 'michaelqin',
    	sayName: function(){ alert(this.name); }
    };
    
    var person = Object.create(Person);
    person.sayName();
    

js类继承的方法有哪些

四种方法:构造,原型链,组合,寄生

  1. 构造继承
  2. 原型链继承
  3. 组合继承
  4. 寄生组合式继承

构造继承

缺陷:

  • 只能继承父类的实例属性和方法,不能继承prototype上的属性/方法

代码实现:

function Animal() {
    this.name = 'animal';
}

Animal.prototype.sayName = function () { 
    console.log(this.name);
};

function Person() {
    Animal.call(this); // 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类
}

let p = new Person()
p.sayName() // TypeError: p.sayName is not a function

原型链继承

缺陷:

  1. 原型链上的引用类型的数据会被所有实例共享
  2. 无法实现多继承

代码实现:

function Animal() { // 父类
    this.name = 'animal'
    this.arr = [1]
}

function Dog() { // 子类
    this.name = 'dog'
}

Dog.prototype = new Animal() // 原型链继承;将父类的实例作为子类的原型;
// 缺陷:虽然实现了继承;但切断了`Dog.prototype.constructor`与`Dog`的关系
Dog.prototype.constructor = Dog // 弥补上面的缺陷

let dog1 = new Dog()
let dog2 = new Dog()
dog1.arr.push(2) // 缺陷:原型链上的引用类型数据会被所有实例共享
console.log(dog1.arr) // [ 1, 2 ]
console.log(dog2.arr) // [ 1, 2 ]

组合继承

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

构造继承 + 原型链继承的组合

推荐:

  • 优点:解决了构造函数继承和原型链继承的缺点
  • 缺点:调用了两次父类的构造函数,即new 父类()使用了两次,原型链继承父类的构造函数一次,实例化一次

代码实现:

function Animal() { // 父类
    this.name = 'animal'
    this.arr = [1]
}

Animal.prototype.sayName = function () {
    console.log(this.name);
};

function Dog(name) { // 子类
    Animal.call(this); // 绑定上下文
    this.name = name || 'huahua'
}

Dog.prototype = new Animal() // 原型链继承;父类实例
Dog.prototype.constructor = Dog // 弥补上面的缺陷

let dog1 = new Dog()
let dog2 = new Dog()
dog1.arr.push(2) 
console.log(dog1.arr) // [ 1, 2 ]
console.log(dog2.arr) // [ 1 ]
dog1.sayName()

寄生组合式继承

核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免组合继承的缺点

构造继承 + 原型链继承 + 父类原型链的浅拷贝

  • 特点:使用到了Object.create(父类.prototype)实现原型链的浅拷贝
  • 优点:解决了原型链继承构造函数继承的缺点
  • 缺点:暂无

代码实现:

function Animal() { // 父类
    this.name = 'animal'
    this.arr = [1]
}

Animal.prototype.sayName = function () {
    console.log(this.name);
};

function Dog() { // 子类
    Animal.call(this); // 构造继承
}

Dog.prototype = Object.create(Animal.prototype) // 对父类prototype的浅拷贝,而不是复制父类的实例
Dog.prototype.constructor = Dog

JS多重继承的实现方法是怎么样的

多继承中典型的问题,Diamond Problem

当 A, B, C 中都定义了一个相同名称的函数时,而在 D 的实例对象中调用这个函数时,究竟应该去执行谁。。。

MRO算法

Method Resolution Order (MRO) 指的是在继承结构中确定类的线性顺序

defineProperty,hasOwnProperty,propertyIsEnumerable都是做什么用的

  • Object.defineProperty(obj, prop, descriptor)用来给对象定义属性,有value,writable,configurable,enumerable,set/get等.
  • hasOwnPrpoerty用于检查某一属性是不是存在于对象本身,继承来的父亲的属性不算.
  • propertyIsEnumerable用来检测某一属性是否可遍历,也就是能不能用for..in循环来取到.

理解原型设计模式以及JavaScript中的原型规则

  1. 所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性;
  2. 所有的引用类型(数组、对象、函数),都有一个__proto__属性(隐式原型),属性值是一个普通的对象;
  3. 所有的函数,都具有一个 prototype(显式原型),属性值也是一个普通对象;
  4. 所有的引用类型(数组、对象、函数),其隐式原型指向其构造函数的显式原型(obj._proto_ === Object.prototype)
  5. 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的 __proto__ (即它的构造函数的 prototype)中去寻找;

为什么要用原型模式

原型模式的好处

  • 工厂模式:不明确创建对象o的类型
  • 构造函数模式:会创建多个完成同样任务的Function实例

instanceof的底层实现原理

instanceof的判断逻辑是:从当前引用的proto一层一层顺着原型链往上找,能否找到对应的prototype。找到了就返回true

简单说就是判断实例对象的__proto__是不是强等于对象的prototype属性,如果不是继续往原型链上找,直到 __proto__ 为 null 为止。

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式 
    var O = R.prototype;   // 取 R 的显示原型 
    L = L.__proto__;  // 取 L 的隐式原型
    while (true) {    
        if (L === null)      
             return false;   
        if (O === L)  // 当 O 显式原型 严格等于  L隐式原型 时,返回true
             return true;   
        L = L.__proto__;    // 隐式原型
    }
}

定时器Timer

任务执行顺序: 同步 > process.nextTick() > Promise > setTimeout(,less time) > setImmediate > setTimeout(,more time)

// test.js
setTimeout(() => console.log(1)); // 4. 而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
setImmediate(() => console.log(2)); // 5. 而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
process.nextTick(() => console.log(3)); // 2. process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。
Promise.resolve().then(() => console.log(4));// 3. process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。
(() => console.log(5))();   // 1. 同步任务总是比异步任务更早执行。

运行结果如下

5 
3
4
1
2

setTimeout和setImmediate,如果setTimeout的时间足够小,则setTimeout先执行,如果为什么?

  1. 查看 Timer源码,setImmediate 和 setTimeout最终都是在一个 时间堆PriorityQueue 上进行执行
  2. 但是,一开始setImmediates先加入队列immediateQueue使用双端链表linkedlist实现
  3. 执行的时候,setImmediates从队列进入 时间堆PriorityQueue,这个过程是有代价的,所有如果 setTimeout是0ms或者说时间足够小,setTimeout先执行

优雅退出

graceful模块配合cluster就可以实现这个解决方案。

graceful是基于domain模块实现的

domain;能捕捉异步回调中出现的异常。(弥补try…catch…的不足)

process.on(‘uncaughtException)

什么是错误优先的回调函数

错误优先的回调函数用于传递错误和数据。第一个参数始终应该是一个错误对象, 用于检查程序是否发生了错误。其余的参数用于传递数据。

只有遵循错误优先回调的函数可以promisify

为何try里面放return,finally还会执行,理解其内部机制

在 try 语句中,在执行 return 语句时,要返回的结果已经准备好了,就在此时,程序转到 finally 执行了。

在转去之前,try 中先把要返回的结果存放到局部变量中去,执行完 finally 之后,在从中取出返回结果。

因此,即使finally 中对返回的结果进行了改变,但是不会影响返回结果。

它应该使用栈保存返回值

为什么用Node.js

  • 简单强大,轻量可扩展
  • javascript,json来进行编码,web开发必备技能
  • 非阻塞IO,可以适应分块传输数据,较慢的网络环境,尤其擅长高并发访问
  • 前后端使用统一语言
  • 可扩展体现在可以轻松应对多实例,多服务器架构,同时有海量的第三方应用组件

node的构架

主要分为三层: 应用app >> V8及node内置架构 >> 操作系统

V8是node运行的环境,可以理解为node虚拟机.

node内置架构又可分为三层: 核心模块(javascript实现) >> c++绑定 >> libuv + CAes + http

node的构架

列举几个核心内置库介绍下其实现原理

常用的核心内置库如事件EventEmitter流Stream文件fs网络net,http,https进程管理process、cluster

EventEmitter

EventEmitter是node中一个实现观察者模式的类,主要功能是监听和发射消息,用于处理多模块交互问题

代码实现

var util = require('util');
var EventEmitter = require('events').EventEmitter;

function MyEmitter() {
    EventEmitter.call(this);
} // 构造函数

util.inherits(MyEmitter, EventEmitter); // 继承

var em = new MyEmitter();
em.on('hello', function (data) {
    console.log('收到事件hello的数据:', data);
}); // 接收事件,并打印到控制台
em.emit('hello', 'EventEmitter传递消息真方便!');

EventEmitter有哪些典型应用

  1. 模块间传递消息
  2. 回调函数内外传递消息
  3. 处理流数据,因为流是在EventEmitter基础上实现的.
  4. 观察者模式发射触发机制相关应用

怎么捕获EventEmitter的错误事件

监听error事件即可.如果有多个EventEmitter,也可以用domain来统一处理错误事件

var domain = require('domain');
var myDomain = domain.create();
myDomain.on('error', function (err) {
    console.log('domain接收到的错误事件:', err);
}); // 接收事件并打印
myDomain.run(function () {
    var emitter1 = new MyEmitter();
    emitter1.emit('error', '错误事件来自emitter1');
    emitter2 = new MyEmitter();
    emitter2.emit('error', '错误事件来自emitter2');
});

EventEmitter中的newListenser事件有什么用处

newListener可以用来做事件机制的反射,特殊应用,事件管理等

当任何on事件添加到EventEmitter时,就会触发newListener事件,基于这种模式,我们可以做很多自定义处理.

var emitter3 = new MyEmitter();
emitter3.on('newListener', function(name, listener) {
	console.log("新事件的名字:", name);
	console.log("新事件的代码:", listener);
	setTimeout(function(){ console.log("我是自定义延时处理机制"); }, 1000);
});
emitter3.on('hello', function(){
	console.log('hello node');
});

Stream

stream是基于事件EventEmitter的数据管理模式.由各种不同的抽象接口组成,主要包括可写,可读,可读写,可转换等几种类型

Stream有什么好处

非阻塞式数据处理提升效率,片断处理节省内存,管道处理方便可扩展等

Stream有哪些典型应用

文件,网络,数据转换,音频视频等

怎么捕获Stream的错误事件

监听error事件,方法同EventEmitter

有哪些常用Stream,分别什么时候使用

  • 可读流Readable,在作为输入数据源时使用;
  • 可写流Writable,在作为输出源时使用;
  • 双工流Duplex,它作为输出源接受被写入,同时又作为输入源被后面的流读出.
  • 转换流Transform,跟Duplex一样,都是双向流,但它的输出与输入是相关联的
    • 需要实现一个函数_transfrom(chunk, encoding, callback);而Duplex需要分别实现_read(size)函数和_write(chunk, encoding, callback)函数

缓冲

highWaterMark

可缓冲的数据大小取决于传入流构造函数的 highWaterMark 选项。 对于普通的流, highWaterMark 指定了字节的总数。 对于对象模式的流, highWaterMark 指定了对象的总数。

限制数据的缓冲到可接受的程度,也就是读写速度不一致的源头与目的地不会压垮内存

实现一个Writable Stream

三步走:

  1. 继承Writable
  2. 覆写原型链方法_write(chunk, encoding, callback)函数

代码实现:

var Writable = require('stream').Writable;
var util = require('util');

function MyWritable(options) {
	Writable.call(this, options); // 构造继承
} 
util.inherits(MyWritable, Writable); // 继承Writable
MyWritable.prototype._write = function(chunk, encoding, callback) {
	console.log("被写入的数据是:", chunk.toString()); // 此处可对写入的数据进行处理
	callback();
};

process.stdin.pipe(new MyWritable()); // stdin作为输入源,MyWritable作为输出源   

文件系统fs

Node通过fs模块来和文件系统进行交互,该模块提供了一些标准的文件访问API类打开、读取、写入文件、以及与其交互。

内置的fs模块架构是什么样子的

fs模块主要由下面几部分组成:

  1. POSIX文件操作的封装,对应于操作系统的原生文件操作; unlink,stat,rename…
  2. 文件流 fs.createReadStream和fs.createWriteStream
  3. 同步文件读写, fs.readFileSync和fs.writeFileSync
  4. 异步文件读写, fs.readFile和fs.writeFile

读写一个文件有多少种方法

总体来说有四种:

  1. POSIX式底层读写
  2. 流式读写
  3. 同步读写
  4. 异步读写

怎么读取json配置文件

  1. 利用node内置的require('data.json')机制 (注意:其中一个改变了js对象,其它跟着改变)
  2. 读入文件入内容,然后用JSON.parse(content)转换成js对象

两种方式的区别:

  1. require机制情况下,如果多个模块都加载了同一个json文件,那么其中一个改变了js对象,其它跟着改变,这是由node模块的缓存机制造成的,只有一个js模块对象
  2. 第二种方式则可以随意改变加载后的js变量,而且各模块互不影响,因为他们都是独立的,是多个js对象

fs.watch和fs.watchFile有什么区别

二者主要用来监听文件变动

  1. fs.watch利用操作系统原生机制来监听,可能不适用网络文件系统;
  2. fs.watchFile则是定期检查文件状态变更,适用于网络文件系统,但是相比fs.watch有些慢,因为不是实时机制.

网络

node的网络模块架构是什么样子的

node全面支持各种网络服务器和客户端,包括http/https, tcp, udp, dns, tls/ssl

node是怎样支持https,tls的

要实现以下几个步骤即可:

  1. openssl生成公钥私钥
  2. 服务器或客户端使用https替代http
  3. 服务器或客户端加载公钥私钥证书

global

全局对象包括:模块变量函数,Buffer类,Timer函数,process, console

模块

  1. __dirname
  2. __filename
  3. exports
  4. module
  5. require()

Buffer类(缓冲器)

timer(定时器)

setTimeout/clearTimeout, setInterval/clearInterval, setImmediate/clearImmediate

console

process

进程管理

child_process, cluster

child_process创建子进程的方式

  • .exec()、.execFile()、.fork()底层都是通过.spawn()实现的。
  • .exec()、execFile()额外提供了回调,当子进程停止的时候执行

风险项

传入的命令,如果是用户输入的,有可能产生类似sql注入的风险,比如

exec('ls hello.txt', function(error, stdout, stderr){}) 恶意攻击 => exec('ls hello.txt; rm -rf *', function(error, stdout, stderr){})

cluster

master 进程通过 process.fork() 创建子进程,他们之间通过 IPC (内部进程通信)通道实现通信。

操作系统的进程间通信方式: 共享内存,消息传递(socket,rpc),信号量,管道,消息队列等

  1. 共享内存;不同进程共享同一段内存空间。通常还需要引入信号量机制,来实现同步与互斥。
  2. 消息传递;这种模式下,进程间通过发送、接收消息来实现信息的同步。nodejs父子进程通过事件机制通信,就是这种模型
  3. 信号量;信号量简单说就是系统赋予进程的一个状态值,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量只有 0 或者 1 两个值的话,又被称作“互斥锁”。这个机制也被广泛用于各种编程模式中。
  4. 管道;用于连接两个进程,将一个进程的输出作为另一个进程的输入

Node.js 为父子进程的通信提供了事件机制EventEmmiter来传递消息。

负载均衡策略

Node.js 默认采用的策略是 round-robin 时间片轮转法。

负载均衡算法:

  • round-robin 时间片轮转法; 每一次把来自用户的请求轮流分配给各个进程,不足:处理效率不一样,会出现负载不均衡
  • WRR (weight-round-robin) 加权轮转法

时间片轮转法(round-robin)不适用于windows, 第二种方式是由主进程创建 socket 监听端口后, 将 socket 句柄直接分发给相应的 worker, 然后当连接进来时, 就直接由相应的 worker 来接收连接并处理;存在负载不均衡的问题, 比如通常 70% 的连接仅被 8 个进程中的 2 个处理, 而其他进程比较清闲.

多进程的端口监听

  1. master 进程负责监听端口
  2. 然后将连接通过某种分发策略(比如 round-robin),转发给 worker 进程。
  3. 这样由于只有 master 进程接收客户端连接,就解决了竞争导致的负载不均衡的问题
  4. 关键:要求 master 进程的稳定性足够好

事件循环和非阻塞IO

单线程

  • 传统web服务中,大多都是使用多线程机制来解决并发的问题,原因是I/O事件会阻塞线程,而阻塞就意味着要等待
  • node的设计是采用了单线程的机制,只是针对 主线程来说,即每个node进程只有一个主线程来执行程序代码
  • 采用了事件驱动的机制,将耗时阻塞的I/O操作交给线程池中的某个线程去完成
  • 主线程本身只负责不断地调度,并没有执行真正的I/O操作。也就是说node实现的是异步非阻塞式。

底层,Node.js借助libuv来作为抽象封装层,从而屏蔽不同操作系统的差异

libuv: linux下用libev实现,Windows下用IOCP实现

IO多路复用模型

事件循环机制

根据node的官方介绍,node每次事件循环机制都包含了6个阶段:

  • timers阶段:这个阶段执行已经到期的timer(setTimeout、setInterval)回调
  • I/O callbacks阶段:执行I/O(例如文件、网络)的回调
  • idle, prepare 阶段:node内部使用
  • poll阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check阶段:执行setImmediate回调
  • close callbacks阶段:执行close事件回调,比如TCP断开连接

对于日常开发来说,我们比较关注的是timers、I/O callbacks、check阶段

  • node和浏览器相比一个明显的不同就是node在每个阶段结束后会去执行所有 process.nextTick 、microtask(promise)任务

事件循环原理

  • node 的初始化
    • 初始化 node 环境。
    • 执行输入代码。
    • 执行 process.nextTick 回调。
    • 执行 microtasks。
  • 进入 event-loop
    • 进入 timers 阶段
    • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
    • 检查是否有 process.nextTick 任务,如果有,全部执行。
    • 检查是否有microtask,如果有,全部执行。
    • 退出该阶段。
    • 进入IO callbacks阶段
    • 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段
    • 检查是否有 process.nextTick 任务,如果有,全部执行
    • 检查是否有microtask,如果有,全部执行
    • 退出该阶段
    • 进入 idle,prepare 阶段:
    • 这两个阶段与我们编程关系不大,暂且按下不表。
    • 进入 poll 阶段
    • 进入 check 阶段
    • 进入 closing 阶段
    • 检查是否有活跃的 handles(定时器、IO等事件句柄)
    • 如果有,继续下一轮循环。
    • 如果没有,结束事件循环,退出程序。

可以发现,在事件循环的每一个子阶段退出之前都会按顺序执行如下过程:

  • 检查是否有 process.nextTick 回调,如果有,全部执行
  • 检查是否有 microtasks,如果有,全部执行
  • 退出当前阶段

同一次事件循环中,微任务永远在宏任务之前执行

宏任务和微任务

任务队列又分为macro-task(宏任务)micro-task(微任务),在最新标准中,它们被分别称为taskjobs

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)

输出: 1,2,3,4

  1. new Promise => 同步执行
  2. Promise.then() => Promise.then()中注册的回调才是异步执行的;具有代表性的微任务
  3. setTimeout就是作为宏任务来存在的
  • 微任务:process.nextTick、Promise.then catch finally
  • 宏任务: I/O,setTimeout、setInterval、setImmediate,script(整体代码),UI rendering

异步事件

非I/O:

  • 定时器(setTimeout,setInterval)
  • process.nextTick
  • microtask(promise)
  • setImmediate
  • DNS.lookup

I/O:

  • 网络I/O
  • 文件I/O
  • 一些DNS操作

如何避免回调地狱

  • 使用三方库,Q, blubird, async进行promisify
  • 使用Promise链式调用
  • 使用yield+生成器generator或Promise, co
  • 使用async, await语法糖ES6

什么是stub

TDD、Stub和Mock

stub存在的意图是为了让测试对象可以正常的执行,硬编码一些输入和输出

mock除了保证stub的功能之外,还可深入的模拟对象之间的交互方式,如:调用了几次、在某种情况下是否会抛出异常

stub是 mock的子集

express的路由机制

express是如何从一个中间件执行到下一个中间件的?

A: app.use()

app.use()的原理是什么?

function middleware(req,res,next){
    // 做该干的事

    // 做完后调用下一个函数
    next(); // next也是一个函数,它表示函数数组中的下一个函数
}

其实所有中间件函数,是顺序添加到中间件数组里面,这个函数数组表示在发出响应之前要执行的所有函数

使用app.use(fn)后,传进来的fn就会被扔到这个数组里,执行完毕后调用next()方法执行函数数组里的下一个函数,如果没有调用next()的话,就不会调用下一个函数了,也就是说调用就会被终止

Express和Koa的区别

  1. Handler 处理方式
    • Express 使用普通的回调函数,一种线性的逻辑,在同一个线程上完成所有的 HTTP 请求;异步操作的执行顺序不确定;回调的方式不利于错误捕获;
    • 使用ES7的Async/Await替换了原来的 Generator + co 的模式; Async/Await 现在也称为 JS 异步的终极解决方案。
  2. 中间件实现机制
    • koa2: 中间件 Compose;洋葱圈模型;会去等待异步(Promise)完成;可以非常方便的实现后置处理逻辑
    • Express 中间件实现是基于 Callback 回调函数同步的,它不会去等待异步(Promise)完成
  3. 响应机制
    • 在 Koa 中数据的响应是通过 ctx.body 进行设置,注意这里仅是设置并没有立即响应,而是在所有的中间件结束之后做了响应;这样做一个好处是我们在响应之前是有一些预留操作空间的,
    • express:res.send() 之后就立即响应了,这样如果还想在上层中间件做一些操作是有点难的。

其实,Express 也是类似的洋葱模型,不同的是:

Express 中间件机制使用了 Callback 实现,这样如果出现异步则可能会使你在执行顺序上感到困惑,因此如果我们想做接口耗时统计、错误处理 Koa 的这种中间件模式处理起来更方便些。最后一点响应机制也很重要,Koa 不是立即响应,是整个中间件处理完成在最外层进行了响应,而 Express 则是立即响应。

egg.js和nest.js

  • egg.js是在koa的基础上做了一层很好的面向大型企业级应用的框架封装
  • egg支持ts
  • egg.js更多的是按照洋葱模型的开发方式,和AOP编程还是有点区别的
  • NEST.js配合typeorm可以在node下拥有不输Spring的面向切面编程的体验

js里的作用域

作用域(scope)就是变量访问规则的有效范围

  • 全局变量的作用域是全局的
  • 函数作用域:一个变量在全函数里有效
  • 块作用域:代码块里有效;{} 限定变量的作用域范围

理解词法作用域和动态作用域

词法作用域也称静态作用域,javascript 采用静态作用域

  • 静态作用域 —— 函数的作用域基于函数创建的位置。
  • 动态作用域 —— 函数的作用域基于函数的使用位置。

垃圾回收

垃圾回收其实就是:GC机制

javascript垃圾回收有两种方法:标记清除、引用计数。引用计数不太常用,标记清除较为常用。

标记清除: 其实就是图的拓扑排序,查找连通图

引用计数有个最大的问题:循环引用

function func() {
    let obj1 = {};
    let obj2 = {};

    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。

obj1 = null;
obj2 = null;

更多GC相关:

  • 经典的GC算法有三种: 引用计数(reference counting)、 标记-清扫(mark-sweep)、 复制收集(CopyandCollection)。
  • Java VM采用的Mark Sweep算法
  • Golang的GC算法主要是基于标记-清扫(markandsweep)算法,并在此基础上做了改进=>三色标记法

标记-清扫(Mark And Sweep)算法存在的问题

  • STW,stop the world;让程序暂停,程序出现卡顿。
  • 标记需要扫描整个heap
  • 清除数据会产生heap碎片

这里面最重要的问题就是:mark-and-sweep 算法会暂停整个程序

如何优化?

三色标记法:通过白色、灰色、黑色三色标记,最后只剩黑色和白色,清除白色;使清除操作和用户逻辑可以并发

内存泄漏

哪些情况会引起内存泄漏?

有了GC同样会出现内存泄露问题!比如如下场景

  1. 全局对象
    • 在 JavaScript 文件头部加上 ‘use strict’
    • 静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。
  2. 各种IO连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
  3. 遗忘的计时器或回调函数
    • 计时器和计时器内部引用的资源都不会被释放
  4. 监听器的使用,在释放对象的同时没有相应删除监听器的
  5. 闭包里的局部变量
  6. 占用CPU过高,造成单线程阻塞,堆积内存过高

避免内存泄漏的一些方式:

  • 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收
  • 注意程序逻辑,避免“死循环”之类的
  • 避免创建过多的对象

总而言之需要遵循一条原则:不用了的东西要及时归还

如何定位内存溢出

定位内存泄漏,通常有两种情况

  1. 正常使用就可以重现的,在测试环境复现解决; 可以使用–inspect和chrome://inspect进行复现
  2. 偶然发生的内存泄漏,一定与某些特殊输入有关。如果不能通过代码日志定位到这个特殊输入,需要在生产环境打印内存快照
  • 快照工具推荐使用heapdump用来保存内存快照,
  • 使用devtool来查看内存快照

在nodejs中如何解决超出最大的调用栈错误

如何分析Node.js中的内存泄漏

平时工作中怎么调优的,怎么解决爆栈

  • 设置程序运行最大内存
  • 做好监控,重启,发现内存泄漏后使用–inspect和chrome://inspect进行定位,无法定位,打印内存快照分析,heapdump,devtool等工具

内存溢出解决方法

  1. 限制最高内存使用,--max-old-space-size--max-new-space-size参数来调整内存大小的使用限制;node –max-old-space-size=8192,意思是将内存调整到8G;node的堆内存上限大概在1.7G这块。
  2. 内存监控,当内存占用达到一定比例,采用优雅退出的方案重启进程

this

  • this指的是函数运行时所在的环境
  • this指的是对象本身,而不是构造函数

    var obj = {
    foo: function () { console.log(this.bar) },
    bar: 1
    };
    
    var foo = obj.foo;
    var bar = 2;
    
    obj.foo() // 1
    foo() // 2
    

apply,call和bind有什么区别

三者都是用来改变函数中this的指向

  • apply和call方法调用之后会立即执行,而bind方法调用之后会返回一个新的函数,它并不会立即执行,需要我们手动执行。
  • apply传参是数组,call,bind传参逗号隔开

列举一些Math,Array,String方法

  • Math.floor() , Math.abs(), Math.random(), Math.pow(x, y)
  • Array.from(), Array.fill(), Array.pop() push() shift() unshift() reverse() sort() join() split() concat() indexOf()
  • .length .charCodeAt() .split() .slice() .substring() .trim()

给String, 添加一个getLength()方法

例如var str =‘sss’; 使得str.getLength()结果为 3

String.propotype.getLength = function (){
 return this.length
}

用 javascript 实现数组去重

let arr = [1, 2, 3, 2, 1, 3, 4, 2]

答: [...new Set(arr)]

闭包

闭包:能够读取其他函数内部变量的函数。(应用场景:要获取某函数内部的局部变量)

闭包的优点:

  1. 能够读取函数内部的变量
  2. 让这些变量一直存在于内存中,不会在调用结束后,被垃圾回收机制回收

闭包的缺点:正所谓物极必反,由于闭包会使函数中的变量保存在内存中,内存消耗很大,所以不能滥用闭包, 解决办法是,退出函数之前,将不使用的局部变量删除。

javascript 删除变量:你可以为其赋值为空值,比如 undefinednull 就相当于删除了(标记清除)

闭包其实就是作用域范围,然后 js的作用域是函数作用域,所以闭包也是函数, 本质是 父子函数的引用关系:父函数包含子函数,子函数因为函数作用域又引用父函数,这是个死结; 由于相互引用,会引起内存泄漏,不用的时候把引用设为null,内存释放,死结解开

应用场景:

  1. 匿名自执行函数
  2. 结果缓存
  3. 封装局部变量

ES6有哪些新特性

  • 类的支持:语法糖,只是让类更加直观;基于ES5的原型链都可以实现;
  • 模块化
    • export和import命令;
    • ES6之前是整体加载一个模块;ES6 模块是编译时加载,使得静态分析成为可能; 进一步可以引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
  • 箭头函数
  • 块作用域
  • 字符串模板
    • 使字符串拼接,多行字符串,变量嵌入更容易书写;
  • 变量的解构赋值
  • 参数默认值/不定参数/拓展参数
    • 好处:方便参数扩展
  • for-of遍历
  • generator
  • Map/Set
  • Promise
  • Symbol: 每个Symbol实例都是唯一的,从根本上防止属性名的冲突;类似于java中的

谈谈个人对ES6的看法:

  • 吹一波,ES6很好
  • 更友好的语法糖,开发效率得以提高;
  • 但是回归本质,不管ES几,还是那个nodejs,特性只是特性,我们搞技术最重要的还是看清事物本质

箭头函数

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数
  • 不可以使用arguments对象,该对象在函数体内不存在
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

块作用域

  • 不存在变量提升;不存在先使用后声明的情况
  • 暂时性死区;变量绑定在当前作用域,不受外部影响;
  • 不允许重复声明;同一作用域不能声明同名变量

变量的解构赋值

解构是ES6提供的语法糖,其实内在是针对可迭代对象的Iterator接口,通过遍历器按顺序获取对应的值进行赋值

只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

支持 Iterator 接口数据结构:

  1. 数组
  2. Map,Set
  3. Generator 函数

注意:

对象没有部署iterator接口:只有成员具有顺序性的 线性结构 才会部署iterator接口,当然也可以手动部署

修饰器

Decorator(注解)

  • 装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法
  • 装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。
  • 通过对目标类的prototype对象操作,修改类
  • 装饰器不仅可以装饰类,还可以装饰类的属性
  • 如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行
  • 装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升

exports 和 module.exports 的区别和关系

每个模块内部,module变量代表当前模块。 这个变量是一个对象,它的exports属性(即module.exports)是对外的接口

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。 var exports = module.exports;

错误处理的常见方法有哪些

一般来说有四种异常捕获方式

  1. 错误优先回调
  2. try …catch …未处理的错误
  3. 自定义error事件;继承EventEmitter,实现.on(‘error’)
  4. 全局异常捕获 process.on(‘uncaughtException’); domain

实现Promise

function Promise(fn){
  var callback
  this.then = function(done){
    callback = done
  }
  function resolve(){
    callback()
  }
  fn(resolve)
}

一个js文件怎么直接调用

.unref()

unref()可以让父进程退出

const { spawn } = require('child_process');

const subprocess = spawn(process.argv[0], ['child_program.js'], {
  detached: true, // 子进程将会在父进程退出后继续运行
  stdio: 'ignore'
});

subprocess.unref(); // 阻止父进程继续等待

这样实际上实现了一个守护进程