第一篇:谈谈JS的数据类型
- 先探讨Javascript的内存模型,再通过typeof这一API来探讨数据类型的分类和历史遗留的现象,来认识该API设计成这样的目的
内存模型
- JavaScript的内存模型主要划分为调用栈,堆,可执行代码区,而栈中存放原始类型的数据,堆中存放引用类型的数据
- 每当运行可执行代码(eval,全局,函数代码)就会生成相应的执行上下文压入执行栈中,每个执行上下文里包含了变量环境(var声明)和词法环境(let声明,with语句,try catch语句,函数声明),this,outer(指向父作用域的引用)等
- 可执行代码在运行时会进行预编译生成执行上下文
- 全局上下文,对应放入变量环境和词法环境
- 先找变量声明,key为变量名,value为undefined
- 再找函数声明,key为函数名,value为函数体
- 执行代码时遇到函数调用,预编译生成相应函数上下文
- 先找形参和变量声明,key为变量名,value为undefined
- 实参传值
- 再找函数声明,key为函数名,value为函数体
- 直到执行完全部代码
- 全局上下文,对应放入变量环境和词法环境
块级作用域
-
let和const允许声明一个作用域被限制在块中的变量、语句或表达式,而var声明的变量只能是全局或整个函数块的
-
在块级作用域中声明函数,不同浏览器的兼容处理不同
类型检测
typeof
-
JavaScript的数据类型可划分为原始类型7个:number,bigint,symbol,string,boolean,undefined,null,其余则为引用类型
-
但通过typeof来判断所属类型时会发现将null识别为object,原因如下:
- 在最初Javascript的实现中,值以32位存储,类型标曲存储在低位中(1-3位)
- 000:对象类型。数据是对象的地址值
- 1:整数。数据是31位有符号整数
- 010:double。数据是对双浮点数的引用
- 100:string。数据是对字符串的引用
- 110:boolean。数据是一个布尔值
- 而
null
代表的是空指针,一般存储在0x00地址中,它的低三位也为000,因此typeof将其判断为对象类型
虽然后来提出了修复方案将其修改为:typeof null === ‘null’,但没有通过
- 在最初Javascript的实现中,值以32位存储,类型标曲存储在低位中(1-3位)
-
特别的typeof将Function识别为function,但它也属于引用类型
Object.prototype.toString
-
通过改变this从而区分具体的引用类型
const type = (obj) => Object.prototype.toString.call(obj).slice(8,-1).toLowerCase(); 复制代码
instanceof
object instanceof constructor
复制代码
-
instanceof用于检测constructor.prototype是否object的原型链上
function myInstanceof(letf,right) { // left必须为引用类型 if (typeof left !== 'object' || !letf) return false; // 返回参数的原型对象 let proto = Object.getPrototypeOf(left); while (true) { // 原型链尽头 if (proto === null) return false; // constructor的原型在object的原型链上 else if (proto === right.prototype) return true; proto = Object.getPrototypeOf(proto); } } 复制代码
原型链
由上述的instanceof的作用引出一个概念,那就是原型链
-
首先得理清构造函数,实例对象,原型对象之间的关系
-
每个引用类型都有一个__ proto __属性指向它的构造函数的prototype属性上(即原型对象)
-
原型对象的构造函数是Object
-
构造函数是Function通过new生成的实例对象
-
-
特别的:Function.__ proto __ 指向Function.prototype,Object.prototype.__ proto __ 指向null
-
- 上图中通过__ proto __属性构成的链条就是所谓的原型链
- Object.hasOwnProperty()不会沿着原型链上寻找,而for … in则会
借助原型链来实现单继承
new运算符究竟做了什么 ?
- 若构造函数的返回值是引用类型,那么返回它,否则返回实例对象
function objectFactory() {
var obj = new Object()
var args = [].slice.call(arguments,1)
var Constructor = arguments[0]
var res = Constructor.apply(obj,args)
obj.__proto__ = Constructor.prototype
return res ? (typeof res === 'object' | typeof res === 'function' ? res : obj) : obj
}
复制代码
① 改变this调用父构造函数
function Parent1() {
this.name = 'parent1'
}
function Son1() {
Parent1.call(this)
this.type = 'son1'
}
复制代码
- 无法继承父构造函数原型链上的属性和方法
② 改变prototype指向
function Parent2() {
this.name = 'parent2'
}
function Son2() {
this.type = 'son2'
}
Son2.prototype = Parent2.prototype
复制代码
- 虽然继承了构造函数原型链上的属性和方法,但它们继承的是同一个原型对象
① + ② + 中间对象
function Parent3() {
this.name = 'parent3'
}
function Son3() {
Parent.call(this)
this.type = 'son3'
}
/*
Object.create相当于
var F = function () {}
F.prototype = Parent3.prototype
Son3.prototype = new F()
*/
Son3.prototype = Object.create(Parent3.prototype)
// 访问Son3.prototype.constructor时候为Parent3,也就是构造函数对不上,因此需要它
Son3.prototype.constructor = Son3
复制代码
- 没有继承父类的静态方法,但一定要按照面向对象的设计思路吗,像golang就是通过组合的方法实现变量和方法的拼装
null&undefined
-
undefined其实是全局对象上的属性,它表示 值存在但不确定 或 当前值不存在但在将来可能会赋值
- 变量定义但为赋值
- 函数参数未穿实参
- 返回值未指定
- 访问对象未定义的属性
-
null是关键字,代表空指针的意思,一般存储在0x00(大多数操作系统不允许访问该地址),delete掉某个引用类型数据跟将它的变量置为null效果相同
- 原型链的终点
- 变量赋值为null,让垃圾回收器gc掉它
-
在隐式类型转换中,Javascript将null转为0或false,而undefined则转为false或NaN
== 比较时会进行隐式类型转换
不同数据类型的存储位置
- 先来判断下原始类型存储在调用栈中,引用类型则存储在堆中这句话的对错
- 其实原始类型中的string和bigint是存储在堆中的,可以通过控制台中Memory的Take heap snapshot打印堆中存储信息(参考绿皮书),后半句则是对的
包装类
-
基本类型通过 . 调用对象上的方法时会通过new来创建基本类型的包装类,如:(123).toString(),’123′.toString(),true.toString()
- 为何不能创建null,undefined,symbol,bigint的包装类,因为它们不能通过new来新建,会报错的
(123).toString() /* var temp = new Object(123) temp.toString() temp = null */ 复制代码
精度 – IEEE 754标准
-
在控制台输入0.1 + 0.2,它并不等于0.3,这是什么原因呢 ?应该如何解决呢 ?
-
JavaScript遵循IEEE 754标准来表示数字的,由十进制0.1或0.2转为二进制是无限循环小数,根据标准截取为一定位数的二进制,然后再将相加后的结果转会十进制,并保留16位有效数字,因此会丢失后面的位
0.1.toPrecision(16)
-
转字符串相加,这里提供一个思路
-
-
详细推荐文章:Gladyu的0.1 + 0.2不等于0.3?为什么JavaScript有这种“骚”操作?
pass by value
-
传递函数参数时,pass by value(value分为原始类型的值,引用类型的地址值)
function moon (obj) { obj.name = 'func-moon'; obj = new Object(); obj.name = 'light'; return obj; } let Obj = { name: 'global-Obj' }; let newObj = moon(Obj); // 引用类型传递地址值 console.log(Obj.name); // func-moon console.log(newObj.name); // light 复制代码
隐式类型转换
-
0,NaN(Not a Number),””,undefined,null转为布尔类型时均为false,其余都为true
NaN !== NaN,因此需要判断可以通过转字符串NaN判断
-
== 比较的隐式转换情况
- 若两方类型相同,则不需要转换,直接比较
- 否则,若两方分别为null和undefined,则返回true
- 否则,若有一方有NaN,则返回false
- 一方为String,另一方为Number,则String转Number再比较
- 一方为Boolean,则Boolean转Number再比较
- 一方为引用类型,另一方为String,Number,Symbol,则引用类型调用原型链上的toString方法再比较
对象的内置函数
-
对象转原始类型是根据什么流程运行的 ?
- 优先调用内置[toPrimitive]函数
- 其次为ValueOf函数
- 再其次为toString函数
- 上述函数都没有,则调用原型链上的toString函数
var obj = { value: 3, valueOf() { return 4; }, toString() { return '5'; }, [Symbol.toPrimitive]() { return 6; } } console.log(obj,obj + 1) 复制代码
-
如何让if(a == 1 && a == 2)条件成立 ?
var a = { value: 0, valueOf() { this.value += 1 return this.value } } if (a == 1 && a == 2) { console.log('true') } 复制代码
第二篇:实现API加深理解
数组方法
-
需要将this转为Object类型
-
通过右移 >>> 运算符保证为Number的整数类型
-
排除密封对象,冻结对象
- 密封对象
阻止添加新属性,并且现有的属性不可配置(删除),原来属性可写就可以改变属性值
- 冻结对象
密封对象 + 属性值不可改变
shift&push
-
前删需要移动数组元素,而后删不用,可通过delete来删除
Array.prototype.shift = function () { let O = Object(this) let len = O.len >>> 0 if (!len) return undefined let shiftEle = O[0] for (let i = 1; i < len; i ++) { O[i - 1] = O[i] } delete O[len - 1] O.length = len - 1 return shiftEle } 复制代码
-
同理前插需要移动元素,后插不需要
-
需注意数组的最大长度为2^32 – 1
Array.prototype.mpush = function (...args) {
let O = Object(this)
let len = O.length
let argsLen = args.length
if ((len + argsLen) > (2 ** 32 - 1)) {
throw new Error('over args length')
}
for (let i = 0; i < argsLen; i ++) {
O[len + i] = args[i]
}
O.length = len + argsLen
return O
}
复制代码
splice
- 从索引start开始依次删除deleteCount个元素,并依次添加进新元素item1,item2,…
- 返回删除元素所构成的数组,可为空数组
array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
复制代码
function movePostElements(array,start,len,deleteCount,addEle) {
let needMoveIndex = Math.abs(deleteCount - addEle.length)
let fromIndex = start + deleteCount
if (deleteCount === addEle.length) return
else if (deleteCount > addEle.length) {
// 将start + deleteCount ~ len的元素前移deleteCount - addEle.length位
for (let i = fromIndex; i < len; i ++) {
if (i in array) {
array[i - needMoveIndex] = array[i]
}
}
// 删除多余元素
for (let j = len - needMoveIndex; j < len; j ++) delete array[j]
} else {
// 后续元素从deleteCount + start开始后移deleteCount - addEle.length
for (let i = len - 1; i >= fromIndex; i --) {
// 要注意从后往前遍历,后移,防止覆盖元素
if (i in array) {
array[i + needMoveIndex] = array[i]
}
}
}
}
function startOver(start,len) {
if (start < 0) {
return len + (start % len)
} else if (start >= len) {
return start % len
} else {
return start
}
}
function deleteCountOver(deleteCount,len,start) {
// 默认删除start ~ len
if (deleteCount <= 0) return 0
else if (deleteCount > (len - start) || deleteCount === undefined) {
return len - start
} else {
return deleteCount
}
}
Array.prototype.splice = function (start,deleteCount,...addEle) {
let O = Object(this)
let len = O.length >>> 0
let deleteArr = new Array(deleteCount)
let addEleLen = addEle.length
// 索引越界处理
start = startOver(start,len)
deleteCount = deleteCountOver(deleteCount,len,start)
// 冻结和密封对象处理
if (Object.isSealed(O)) {
throw new TypeError('this is sealed object')
} else if (Object.isFrozen(O)) {
throw new TypeError('this is a frozen object')
}
// 拷贝需删除的元素
for (let i = 0; i < deleteCount; i ++) {
// 防止需要删除的元素索引溢出
let index = start + i
if (index in O) {
deleteArr[i] = O[index]
}
}
// 预留空位
movePostElements(O,start,len,deleteCount,addEle)
// 添加元素
for (let i = 0; i < addEleLen; i ++) {
O[start + i] = addEle[i]
}
O.length = len - deleteCount + addEleLen
return deleteArr
}
复制代码
filter&map&reduce
-
通过for循环再配合in运算符来遍历length数量的元素
for in是沿着原型链遍历可枚举属性
for of是遍历可迭代对象本身的数据
Array.prototype.myMap = function (callbackFn,thisArg) {
if (typeof callbackFn !== 'function') {
throw TypeError(callbackFn + 'is not a function')
}
if (this === null || this === undefined) {
throw TypeError("Cannot read property 'map' of null or undefined")
}
let O = Object(this)
let len = O.length >>> 0
let res = new Array(len)
/*
for in是沿着原型链遍历所有可枚举属性
in则是判断该属性是否在原型链上
for of遍历可迭代对象本身的数据
*/
for (let i = 0; i < len; i ++) {
if (i in O) {
res[i] = callbackFn.call(thisArg,O[i],i,O)
}
}
return res
}
Array.prototype.myFilter = function (callbackFn,thisArg) {
if (!this) {
throw new TypeError("not 'filter' of null or undefined")
}
if (typeof callbackFn !== 'function') {
throw new TypeError(callbackFn + "is not a function")
}
let O = Object(this)
let len = O.length >>> 0
let resLen = 0
let res = []
for (let i = 0; i < len; i ++) {
if (i in O) {
if (callbackFn.call(thisArg,O[i],i,O)) {
res[resLen ++] = O[i]
}
}
}
return res
}
Array.prototype.reduce = function (callbackFn,initialValue) {
if (!this) {
throw new TypeError("this is not 'map' of null or undefined")
}
if (typeof callbackFn !== 'function') {
throw new TypeError(callbackFn + "is not function")
}
let O = Object(this)
let len = O.length >>> 0
let acmu = initialValue
let index = 0
if (acmu === undefined) {
// acmu初始化为第一个元素
for (; index < len; index ++) {
if (index in O) {
acmu = O[index ++]
break
}
}
if (index >= len) throw new Error('Each element of the array is empty')
}
for (; index < len; index ++) {
if (index in O) {
acmu = callbackFn.call(undefined,acmu,O[index],index,O)
}
}
return acmu
}
复制代码
- 其它数组方法的实现也是大同小异,但一定需要注意细节,参照mdn上的Polyfill以及测试用例
sort
-
为何元素个数少的时候采用直接插入排序 ?
- 插入排序虽然时间复杂度为O(n^2),而快排则为O(n*logn),但这是针对n很大的情况下,当元素个数少时,采用插入排序比快排的效果会更好
-
元素个数多的时候为何要大费力气寻找哨兵元素 ?
- ciwei的快速排序时间复杂度,通过选取合适的哨兵元素来尽可能地规避快排地最坏时间复杂度出现
-
V8源码中的sort函数实现思路
- 原生的sort方法可以通过comparefn的返回值来控制升序还是降序
comparefn(a,b)
- 如果是return a – b,那么数组就会升序排列。首先在直接插入排序中用comparefn(arr[j],newEle) > 0来判断是否后移元素,当arr[j] > newEle,进行后移使得大的元素就会在前,下面应用到comparefn函数的分析同理
- return b – a,降序分析同理
Array.prototype.msort = function (comparefn) {
let array = Object(this)
let length = array.length >>> 0
// 1. 比较函数未传入,传入默认升序函数
if (typeof comparefn !== 'function') {
comparefn = function (a,b) {
return a - b
}
}
// length < 10,直接插入排序中利用comparefn来进行升降序的控制
const insertSort = (arr,start = 0,end) => {
end = end || arr.length
for (let i = start + 1; i < end; i ++) {
let newEle = arr[i]
let j
for (j = i; j > start && (comparefn(arr[j - 1],newEle) > 0); j --) {
arr[j] = arr[j - 1]
}
arr[j] = newEle
}
return arr
}
// lenght > 1000,getSentryIndex:获取合理的哨兵元素位置
const getSentryIndex = (arr,from,to) => {
let tmpArr = []
let increment = 200 + ((to - from) & 15)
let j = 0
from += 1
to -= 1
// 每隔200 ~ 215个元素放入[索引,值]
for (let i = from; i < to; i += increment) {
tmpArr[j ++] = [i,arr[i]]
}
// 排序,返回中位数索引
tmpArr.sort(function(a,b) {
return comparefn(a[1],b[1])
})
let sentryIndex = tmpArr[tmpArr.length >> 1][0]
return sentryIndex
}
// 2. 排序: from为数组其实索引,to为数组长度
const quickSort = (arr,from,to) => {
let sentryIndex = 0
while (true) {
if (to - from <= 10) {
insertSort(arr,from,to)
return
} else if (to - from > 1000) {
sentryIndex = getSentryIndex(arr,from,to)
} else {
// 通过有符号整数右移运算符,获取中位数
sentryIndex = from + ((to - from) >> 1)
}
// 为了再次确保sentryIndex不是最值,把from,sentryIndex,to - 1再次进行排序取中间数
let v0 = arr[from]
let v1 = arr[to - 1]
let v2 = arr[sentryIndex]
// 三个数通过函数来进行升序排序(a - b),降序排列(b - a)
// 下列注释用comparefn返回值为第一个参数减第二个参数注释
let c01 = comparefn(v0, v1)
if (c01 > 0) {
// v0 > v1,交换
let tmp = v0
v0 = v1
v1 = tmp
} // v1 >= v2
let c02 = comparefn(v0,v2)
if (c02 >= 0) {
// v1 >= v0 >= v2,升序排列v0,v1,v2
let tmp = v0
v0 = v2
v2 = v1
v1 = tmp
// v2 >= v1 >= v0
} else {
// v1 >= v0 && v2 > v0
let c12 = comparefn(v1,v2)
if (c12 > 0) {
// v1 > v2 > v0,交换v1和v2
let tmp = v1
v1 = v2
v2 = tmp
// v2 > v1 > v0
}
}
console.log(v0,v1,v2)
arr[from] = v0
arr[to - 1] = v2
// 记录哨兵
let sentry = v1
// 需要遍历的区间索引: 在此寻找比哨兵小的元素
let low_end = from + 1
let high_start = to - 1
// 交换哨兵与第一个元素,因为哨兵的选取是from,sentryIndex,to - 1经过排序后选中间的,因此第一个元素必然大于第0个元素
arr[sentryIndex] = arr[low_end]
arr[low_end] = sentry
for (let i = low_end + 1; i < high_start; i ++) {
let element = arr[i]
// 从前往后找比哨兵小的元素
let order = comparefn(element,sentry)
if (order < 0) {
// sentry > arr[i],交换元素
arr[i] = arr[low_end]
arr[low_end] = element
low_end ++
} else if (order > 0) {
// sentry < arr[i]
do {
// 从后往前寻找比哨兵小的元素
high_start --
if (high_start == i) break
order = comparefn(arr[high_start],sentry)
} while(order > 0)
/*
为何不直接跟哨兵元素交换 ?
因为可能跟哨兵元素相等 + 哨兵要往前走动!
*/
// 跟哨兵后一个交换元素
arr[i] = arr[high_start]
arr[high_start] = element
if (order < 0) {
// 若小于哨兵元素,那么交换
element = arr[i]
arr[i] = arr[low_end]
arr[low_end] = element
low_end ++
}
}
}
// 元素个数少的区间进行递归,多的继续循环
if (to - high_start < low_end - from) {
quickSort(arr,high_start,to)
to = low_end
} else {
quickSort(arr,from,low_end)
from = high_start
}
}
}
quickSort(array,0,length)
return array
}
const test_arr = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
function comparefn(a, b) {
return b - a
}
test_arr.msort(comparefn)
console.log(test_arr)
复制代码
this
优先级:new > bind > call,apply > 对象.方法 > 直接调用
- ① new调用this指向实例对象
- ② bind,call,apply改变this指向
- ③ 对象调用this指向该对象
- ④ 直接调用this默认指向window,严格模式下指向undefined
- ⑤ 箭头函数没有上下文,寻找最近的上下文
bind
-
绑定this并给予初始参数,返回fBound
- fBound若作为构造函数(new运算符调用),this指向构造函数的实例对象
- fBound作为普通函数,采用绑定的this
Function.prototype.bind = function (context,...initialArgs) { let func = this /* fBound作为构造函数:忽略context fBound作为普通函数:this => context */ let fBound = function (...args) { return func.call( this instanceof func ? this : context, initialArgs.concat(args) ) } // 保证相同原型 fBound.prototype = Object.create(func.prototype) return fBound } 复制代码
call和apply
-
利用③对象.方法:给指定的this添加一个属性,执行完后再删除
Function.prototype.call = function (context, ...args) { context = context || window context.fn = this let res = context.fn(...args) delete context.fn return res } Function.prototype.apply = function (context, args) { context = context || window context.fn = this let res = context.fn(...args) delete context.fn return res } 复制代码
深克隆
-
通过递归实现简易版克隆
function deepClone(target) { if (typeof target !== 'object' || !target) return target let cloneTarget = Object.prototype.toString.call(target).slice(8,-1) === 'Object' ? {} : [] for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]) } } return cloneTarget } 复制代码
-
循环引用(针对引用类型)使得call stack溢出
- 通过有序哈希表new WeakMap()来判断是否需要递归,用WeakMap是因为弱引用能及时gc,防止内存泄漏
-
不能拷贝除Array和Object的其它引用类型,如:Function,RegExp,Date,Error,Map,Set,Symbol等
json.parse(json.stringify())同样面临一样的问题
const getType = (target) => Object.prototype.toString.call(target).slice(8,-1) const weakmap = new WeakMap() /* 可遍历对象:Map,Set,Array,Object,Arguments 不可遍历对象:Number,String,Boolean,Date,RegExp,Function,Error,Symbol */ const canItreable = { "Map": true, "Set": true, "Array": true, "Object": true, "Arguments": true } const handleFunc = (func) => { // 箭头函数返回自身 if (!func.prototype) return func // 非箭头函数,new Function ([arg1[, arg2[, ...argN]],] functionBody) const funcString = func.toString() // ?<=是反向预查,?=是正向预查 const bodyReg = /(?<={)(.|\n)+(?=})/m const argsReg = /(?<=\().+(?=\)\s+{)/ const args = argsReg.exec(funcString) const body = bodyReg.exec(funcString) if (!body) return null if (args) { const argsArr = args[0].split(',') return new Function(...argsArr,body[0]) } else { return new Function(body[0]) } } const handleNoIterable = (target,type) => { const Ctor = target.constructor switch (type) { case 'Error': // new Error([message[, fileName[,lineNumber]]]) return new Ctor(target.message) case 'Date': case 'RegExp': return new Ctor(target) case 'Function': return handleFunc(target) } } function deepClone(target) { // 原始类型 if (( typeof target !== 'object' && typeof target !== 'function') || !target ) return target else { // 引用类型 let type = getType(target) if (!canItreable[type]) { // 不可迭代对象: Number,String,Boolean,Date,RegExp,Function,Error,Symbol return handleNoIterable(target,type) } else { // 可迭代对象: Map,Set,Array,Object,Arguments // 解决因为循环引用而递归爆栈的问题 if (weakmap.get(target)) return target weakmap.set(target,true) // 保证原型相同 let cloneTarget = new target.constructor() // Map,Set if (type === 'Map') { target.forEach((item,key) => { cloneTarget.set(deepClone(key),deepClone(item)) }) } else if (type === 'Set') { target.forEach((item) => { cloneTarget.add(deepClone(item)) }) } else { // Array,Object,Arguments:无法遍历Symbol属性 for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]) } } // 单独处理Symbol作为key const symKeys = Object.getOwnPropertySymbols(target) if (symKeys.length) { symKeys.forEach(symKey => { cloneTarget[symKey] = deepClone(target[symKey]) }) } } return cloneTarget } } } let s1 = Symbol('s1') let date1 = new Date() let obj = { str: '100', un: undefined, nu: null, sym: Symbol(2), reg: /^\d+$/, date: date1, bool: true, arrowFn: () => { console.log('arrow => function') }, map: new Map([['key1','value1'],['key2','value2']]), set: new Set(['set1','set2']), err: new Error('error'), arr: [10, 20, 30], obj: { name: 'cherry', [s1]: 's1' }, func: function fn() { console.log('fn'); } } obj.h = obj let obj2 = deepClone(obj) console.log(obj, obj2) 复制代码
-
EventEmitter
发布-订阅模式
-
采用什么样的结构来存储不同事件类型的回调呢 ?
this._event = Object.create(null) { 'newListener': [cb1,cb2,...,cbn] 'removeListener': [cb1,cb2,...,cbn] ... 'otherEvent': [cb1,cb2,...,cbn] } 复制代码
回调函数执行传参数type和cb
- 先看看on方法,触发newListener事件,绑定相应事件类型的回调
EventEmitter.prototype.on = function (type,cb,flag) { if (!this._event) { this._event = Object.create(null); } EventEmitter['newListener'] && EventEmitter['newListener'].forEach(fn => { fn(type,cb); }) if (this._event[type]) { if (flag) { this._event[type].unshift(cb); } else { this._event[type].push(cb); } } this._event[type] = [cb]; } 复制代码
- 再看once方法,通过用新函数wrap包含cb函数从而实现调用一次后移除(中间件)
EventEmitter.prototype.once = function (type,cb,flag) { function wrap() { this.removeListener(type,cb); cb(...arguments); } wrap.listen = cb; this.on(type,wrap,flag); } 复制代码
- emit和removeListener也是同理
EventEmitter.prototype.emit = function (type, ...args) { if (this._event[type]) { this._event[type].forEach(fn => fn.call(this,...args)) } } EVentEmiiter.prototype.removeListener = function (type,cb) { this.emit('removeListener',type,cb.listen ? cb.listen : cb) if (this._event[type]) { this._event[type].forEach((fn,index) => { if (fn == cb || fn == cb.listen) { this.splice(index,1) } }) } } 复制代码
观察者模式
- 每个subject都会记录观察它的人
subject.observerList = []
复制代码
- 一旦subject发生变化,就通知所有observer(调用每个观察者的回调函数)
subject.notify()
复制代码
- 每个subject都可以有1个或多个observer
subject.addObserver(observer1,oberver2,...)
复制代码
总结
-
发布-订阅与观察者模式的区别在于有没有中间人负责处理双方的事情
第三篇:谈谈与内存相关的概念
闭包
总结内容:zxg_神说要有光【JavaScript 的静态作用域链与“动态”闭包链】
-
谈及闭包,首先需要明确Javascript的作用域分为全局,块级,函数作用域,而那些在声明时就能确定关系的作用域所构成的称为静态作用域链(如:函数作用域链),需要运行时方能确定关系的则是动态作用域链(如:eval)
-
认识作用域链后,需要搞懂一个问题就是为何要引入闭包 ?
- 如果不引入闭包,那么当父函数执行完毕后,那就不能删除它的函数上下文,这明显是不合理的,因此就引入了闭包的机制,这时候就能将它的函数上下文删除掉了
- 返回内部定义的函数到外界
- 内部定义的函数作为外界函数的参数调用
- 如果不引入闭包,那么当父函数执行完毕后,那就不能删除它的函数上下文,这明显是不合理的,因此就引入了闭包的机制,这时候就能将它的函数上下文删除掉了
-
闭包:静态作用域链中,返回函数时需要先扫描函数内部的标识符引用,然后沿着作用域链寻找用到的标识符,将其打成closure包,然后将其存入**[[Scope]]数组中,打完所有包后,JS引擎会根据该数组给该函数形成新的作用域链**
-
这样原有的父函数作用域就可以在调用完后将其销毁,而又能解决子函数需要用到父函数作用域中的标识符,而不将父函数销毁的问题
-
Global和Script对象是一定会打包的
- 无法分析直接调用eval所需要的外部标识符引用,因此需要将整个作用域链的作用域打包(自身的函数调用时会进行预编译形成相应的AO)
- 直接调用eval所形成的闭包巨大,如果很多这样的情况就会导致内存泄漏,需要及时gc形成闭包的函数 => V8垃圾回收机制
- 间接调用eval,仅仅打包变量即可
-
栈的回收机制
-
栈中有一个ESP指针:指向当前执行栈的栈顶,当一个执行上下文执行完成后,可以通过移动ESP指针使得它的内存失效,后续再有其它执行上下文时就可以直接覆盖它
V8垃圾回收机制
详细请阅读:【V8的垃圾回收机制】
-
JavaScript是一门自动垃圾回收的语言,即我们无法主动回收堆中已分配的内存,而说到垃圾回收,首先要提及的就是
代际假说
- 大部分对象都是朝生夕死的
- 不死的对象会活的更久
-
用
引用计数法
来gc的,而当存在循环引用
时,会导致内存泄漏- 采用引用计数法在函数调用完后,由于obj1和obj2仍然相互引用,因此obj1和obj2对象无法被垃圾回收器gc,长久下去会导致一定的内存泄漏
function moon() { let obj1 = { name: 'obj1' }; let obj2 = { name: 'obj2' }; obj1.relate1 = obj2; obj2.relate2 = obj1; } moon(); 复制代码
-
而采用
标记清除法
来gc,同样对于上述的案例,就算存在循环引用,函数调用完后因为obj1和obj2被释放了,所以不能从根节点遍历到他们两个,即这两个对象为垃圾数据
-
而标记清除法的变体
标记整理
主要是为了gc的同时清除相应的内存碎片 -
而V8的垃圾回收器则分为主垃圾回收器和副垃圾回收器,前者负责老生代的gc,后者负责新生代的gc
-
副垃圾回收器采用的是由半空间收集器的变体Scavenge算法
该算法将新生代划分为相等的from space和to space
新对象进入from space,等到from space满时,将from space的
存活对象有序移动
到to space中,然后from space变为to space,而to space变为from space 第一次Scavenge算法存活下来的对象由婴儿变为中年,而第二次Scavenge算法则由 中年变为老年,即由新生代移动到老生代中(而这个就是晋升策略)
-
主垃圾回收器则采用标记清除法以及标记整理法(清除内存碎片,内存碎片过多时触发)
-
-
而由于JavaScript是单线程的,那么每次gc时需要停止JS的执行,而这样的间隙停顿会让用户感受到明显的停顿STW,若需要gc的垃圾数据过多更是如此,因此V8引擎采取了一系列的优化策略减缓这个停顿
- 并行回收,主线程和辅助线程同时gc
- 并发回收,让辅助线程负责gc
- 增量回收,采用三色标记法来进行增量回收,如果在gc一段后,执行的JS将标记为黑色的指向标记为白色的,那么就将白色节点强行变为灰色
-
副垃圾回收器采用并行回收,主垃圾回收器则是三者混合
第四篇:原理篇
Event Loop
Browser
-
浏览器中其它进程完成任务后通过IPC进程通信发布任务给渲染器进程,它里面的IO线程负责进行将任务进行分类,定时器任务放入延时队列,其它宏任务(垃圾回收任务,解析DOM,执行js等)放入消息队列中
-
等渲染器主线程依次执行解析DOM,形成styleSheet,形成layout tree,layer tree,paint Records任务后
-
通知合成线程开始paint,将图层分块,通知光栅化线程池进行GPU raster
-
完成后发送draw quad的命令给浏览器进程让其显示经过GPU光栅化的位图
-
-
等待上述任务完成后,每一轮event loop流程如下
-
① 从消息队列中取出宏任务进行执行
-
② 执行完成后,检查该宏任务的微任务队列,有则执行直到为空
执行微任务的时候会保留宏任务执行的环境 — 全局上下文
-
③ 检查是否需要重新渲染界面
- rAF
- render
-
④ 检查是否有Web Workers的任务需要处理,有则执行
-
⑤ 依次执行延时队列中到期的宏任务,每执行完一个宏任务就回到②,直到把所有到期的定时器任务执行完毕才开始下一轮event loop
-
-
每帧的刷新频率是固定的,如果一帧的剩余时间小于重新渲染所需的时间,那么就会导致丢帧,而react由同步更新转变为异步可中断更新也是为了防止执行递归消耗时间而导致丢帧做的处理,对此浏览器提供了requestIdleCallback可在每帧的空闲时间执行回调
区分jank,reflow,repaint,composition
-
JS动画与CSS3动画的区别 ?
- 通过定时器配合JS修改DOM的几何信息,从而实现动画效果,会触发reflow,同时还有可能会丢帧
后台页面的定时器执行最小时间间隔为1000ms,而rAF在后台页面不会执行
- 后来浏览器提供了rAF来实现动画效果,根据下图,rAF在重新渲染前调用,但阅读此文章rAF回调时机,测试下列代码发现,不同浏览器实现情况却是不一样
- 而可以通过CSS的动画属性transform: xxx等来实现动画,可以跳过布局阶段和Paint,交由合成线程来完成动画效果
Node
-
node版本 < 11,在每个阶段切换或main line执行完,执行nextTickQueue和microTaskQueue
timer1,timer2,promise1,promise2
-
下图是node版本 >= 11的,执行完一个宏任务就去执行nextTickQueue和microTaskQueue
timer1,promise1,timer2,promise2
setImmediate(() => { console.log('timer1') Promise.resolve().then(function () { console.log('promise1') }) }) setImmediate(() => { console.log('timer2') Promise.resolve().then(function () { console.log('promise2') }) }) 复制代码
-
计算轮询超时时间,若loop中没有异步任务,则采用最近的定时器作为允许超过的时间(范围[ 0, INT_MAX ]),即下图的diff
详情可以阅读:Node.js源码解读:深入Libuv理解事件循环 和 从源码解读Node事件循环
-
执行完一个宏任务,就去循环执行nextTickQueue和microTaskQueue
Promise.resolve().then(() => console.log('p1')); process.nextTick(() => console.log('t1')); Promise.resolve().then(() => { console.log('p2'); process.nextTick(() => console.log('t1 inside')); process.nextTick(() => console.log('t2 inside')); }); Promise.resolve().then(() => { console.log('p3') process.nextTick(() => console.log('t3 inside ')); process.nextTick(() => console.log('t4 inside ')); }); setTimeout(() => console.log('omg! setTimeout was called'), 0); setImmediate(() => console.log('omg! setImmediate also was called')); console.log('started'); /* started t1 p1 p2 p3 t1 inside t2 inside t3 inside t4 inside omg! setTimeout was called omg! setImmediate also was called */ 复制代码
- 只有当两个队列都为空才会切换到下一个阶段,因此递归产生process.nextTick或微任务会造成其它任务队列的饥饿现象
-
setTimeout和setImmediate执行顺序的随机性,为何会出现这样的现象呢 ?
setImmediate(function () { console.log('setImmediate') }) setTimeout(function () { console.log('setTimeout') }) 复制代码
-
若机器的性能一般,在进入timers阶段时,已经超过1ms,那么就有timer达到了下限时间([1 ~ max]),因此会执行它,之后再来到check阶段执行setImmediate
打印:setTimeout setImmediate
-
若机器性能很好,在进入timers阶段时,仍未超过1ms,那么就说明没有timer达到了下限时间([1 ~ max]),而且在之后poll阶因为存在setImmediate事件,因此切换到check阶段依次执行所有setImmediate事件
打印:setImmediate setTimeout
-
async/await
-
有了事件循环的概念后结合async/await的使用一开始让我一脸懵逼,后面阅读了该文章,才理清了:探讨:当Async/Await的遇到了EventLoop 和 async/await 在chrome 环境和 node 环境的 执行结果不一致,求解
-
直接说最重要的知识点就是:new Promise((rs,rj) => rs( )) 在浏览器内部会进行什么操作 ?
-
① 插入PromiseResolveThenablejob微任务
-
② 当other-promise处于settled,会立即插入追随者的resolvePromise微任务
let promiseA = new Promise(resolve => resolve('success-promiseA')) let promiseB = new Promise((rs,rj) => rs(promiseA)) // 上述就称promiseB状态跟随promiseA,可以理解为在执行PromiseResolveThenablejob微任务中做了如下伪代码: promiseA.then(resolvePromiseB,rejectPromiseB) 复制代码
-
-
而Promise.resolve( )则直接返回other-promise,这就能解析为何两者间会差两个时序的原因了
let promiseA = new Promise(res =>{ res('promiseA-executor') }) let promiseB = new Promise(res => { // 1. 插入PromiseResolveThenable微任务 res(promiseA) // 3. 1执行后因为promiseA已经处于settled,因此发布resolvePromiseB微任务 }) promiseB.then((value) => { // 5. 3执行后发布(value) => { console.log(value,'pB-then') }微任务 console.log(value,'pB-then') }) Promise.resolve(promiseA).then(() => { // 2. 发布() => { console.log('pl-then') }微任务 console.log('p1-then') }).then(() => { // 4. 2执行后发布() => { console.log('p2-then') }微任务 console.log('p2-then') }).then(() => { // 6. 4执行后发布() => { console.log('p3-then') }微任务 console.log('p3-then') }).then(() => { // 7. 6执行后发布() => { console.log('p4-then') }微任务 console.log('p4-then') }) 复制代码
最终依次打印:p1-then p2-then promiseA-executor PB-then p3-then p4-then
-
区分不同Chrome版本对await v的处理不一样,如下例子
let p = Promise.resolve('moon') async function f() { let temp = await p console.log(temp,'ok') let cmp = await p console.log(cmp,'ok') return Promise.resolve('return-value') } console.log(f()) 复制代码
-
等价转换: [async/await 在chrome 环境和 node 环境的 执行结果不一致,求解](async/await 在chrome 环境和 node 环境的 执行结果不一致,求解)
async function f() { let va = await p console.log('ok') } function transform_f() { return RESOLVE(p).then(value => { let va = value console.log('ok') }) } 复制代码
- Chrome71版本及以下的
- ① await v 等价于 new Promise((rs,rj) => rs(v))
- ② await v后面的代码相当于new Promise((rs,rj) => rs(v)).then(() => { 后面的代码 })
- 隐式返回一个Promise,最终状态由then成功的回调的返回值确定
let p = Promise.resolve('moon') function f() { return new Promise(rs => rs(p)).then(value => { let temp = value console.log(temp,'ok') return new Promise(rs => rs(p)).then(value => { let cmp = value console.log(cmp,'ok') return Promise.resolve('return-value') }) }) } console.log(f()) 复制代码
- Chrome73版本及以上的
- await v 等价于Promise.resolve(v)
- await v 后面的代码相当于Promise.resolve(v).then(() => { 后面的代码 })
- 隐式返回一个Promise,最终状态由then成功的回调的返回值确定
let p = Promise.resolve('moon') function f() { return Promise.resolve(p).then(value => { let temp = value console.log(temp,'ok') return Promise.resolve(p).then(value => { let cmp = value console.log(cmp,'ok') return Promise.resolve('return-value') }) }) } console.log(f()) 复制代码
- 最后附上该文章 探讨:当Async/Await的遇到了EventLoop 的最后留下的题目分析【链式 + 状态跟随】
const promiseA = new Promise((resolve, reject)=>{ resolve( 'ccc' ) }) const promiseB = new Promise((resolve, reject)=>{ // 1. 发布PromiseResolveThenableJobB微任务 resolve( promiseA ) // 6. 执行1 + promiseA处于settled,发布resolvePromiseB微任务 }) const promiseC = new Promise((resolve, reject)=>{ // 2. 发布PromiseResolveThenableJobC微任务 resolve( promiseB ) // 8. 执行6 + promiseB处于settled,发布resolvePromiseC微任务 }) const promiseD = new Promise((resolve, reject)=>{ // 3. 发布PromiseResolveThenableJobD微任务 resolve( promiseC ) // 11. 执行8 + promiseC处于settled,发布resolvePromiseD微任务 }) promiseD.then(()=>{ // 14. 执行11后,发布() => { console.log('promiseD then') }微任务 console.log( 'promiseD then' ) }) promiseC.then(()=>{ // 12. 执行8后,发布() => { console.log('promiseC then') }微任务 console.log( 'promiseC then' ) }) promiseB.then(()=>{ // 9. 执行6后,发布() => { console.log('promiseB then') }微任务 console.log( 'promiseB then' ) }) promiseA.then(()=>{ // 4. 发布() => { console.log('promiseA then') }微任务 console.log( 'promiseA then' ) }) Promise.resolve().then(()=>{ // 5. 发布() => { console.log('p.then1') }微任务 console.log( 'p.then1' ) }).then(()=>{ // 7. 执行5后,发布() => { console.log('p.then2') }微任务 console.log( 'p.then2' ) }).then(()=>{ // 10. 执行7后,发布() => { console.log('p.then3') }微任务 console.log( 'p.then3' ) }).then(()=>{ // 13. 执行10后,发布() => { console.log('p.then4') }微任务 console.log( 'p.then4' ) }).then(()=>{ // 15. 执行13后,发布() => { console.log('p.then5') }微任务 console.log( 'p.then5' ) }).then(()=>{ // 16. 执行15后,发布() => { console.log('p.then6') }微任务 console.log( 'p.then6' ) }) // 打印:promiseA then p.then1 p.then2 promiseB then p.then3 promiseC then p.then4 promiseD then p.then5 p.then6 复制代码
- Chrome71版本及以下的
Promise
-
值传递,异常穿透都是通过返回默认函数完成
Promise.prototype.then = function (onResolved,onRejected) { if (typeof onResolved === 'function') { onResolved = (value) => value } if (typeof onRejected === 'function') { onRejected = (reason) => { throw reason } } ... } 复制代码
-
创建的Promise实例由unsettled => settled是在异步任务中完成的情况,注意需要通过回调数组来存储
-
对then方法不同返回值情况的处理
function Promise(executor) { const self = this ... this.callbackQueue = { fullfilled: [], rejected: [] } function resolve(value) { ... self.callbackQueue .fullfilled.forEach(fn => setTimeout(() => fn(value))) } function reject(value) { ... self.callbackQueue .rejected.forEach(fn => setTimeout(() => fn(value))) } } Promise.prototype.then = function (onResolved,onRejected) { ... if (self.PromiseState === 'pending') { // callback用于处理onResolved,onRejected中不同的返回值情况 self.callbackQueue.fullfilled.push(callback(onResolved)) self.callbackQueue.rejected.push(callback(onRejected)) } } 复制代码
用setTimeout宏任务模拟微任务始终会有所差异
Other
感想
- 之前所学的知识有点零碎,偶然间看了三元的文章,应该注重于知识体系的构建,因此就总结这有了这篇JavaScript的总结,总结发现收获还挺大的,也发现还有许多知识不太懂,接下来也要继续沉淀下来构建知识体系
一些题目
还是通过去做算法题,提升这方面的解决能力比较好
-
字符串去重
hashTable | 对象 | 正则
// RegExp
var str = "sjakxckjsafiwnxmkdabdshanhfowefkbafgber";
var reg = /(.)(?=.*\1)/gs;
str.replace(reg,"");
复制代码
-
找到字符串中第一个只出现一次的字母 ?
队列 + 统计数组
-
封装insertAfter函数
parentNode.insertBefore ( newNode, referrenceNode )
function insertAfter(newNode,referrenceNode) {
// this -> dom
var children = this.children;
var len = children.length;
for (var i = 0; i < len; i ++) {
if (children[i] === referrenceNode) {
this.insertBefore(newNode,children[i].nextElementSibling);
}
}
}
复制代码
- 尝试优化以下代码,使其看起来更加优雅
const actionMap = () => {
const print = (date) => document.write(date);
return new Map([
[/\d/,print(date)],
]);
}
function getDay(day) {
/*
switch(day) {
case 1:
document.write("Monday");
break;
case 2:
document.write("Tuesday");
break;
case 3:
document.write("Wednesday");
break;
...
}
*/
[...actionMap()].forEach(([key,value]) => {
if (key.test(day)) value.call(this,day);
});
}
复制代码
-
数组扁平化
每次扁平化一层,然后递归实现
尾递归优化
阮一峰:【尾调用优化】
-
调用一个函数的时候会将该函数上下文压入执行栈中,而当存在非尾调用递归时,由于此时需要记录调用位置 、变量等信息,以便递归函数结束后能返回到该位置继续执行函数,因此该函数上下文不会被销毁。
-
而这样,在非尾调用递归时,就很容易导致执行栈溢出,即空间复杂度为O(n)
尾调用即在函数的最后一步执行的是函数调用的操作
-
因为已经所处函数的最后一步,即执行完这一步后该函数的调用就结束了。
-
因此不需要去记录调用位置 、变量等相关信息,可以直接将它的函数上下文从执行栈中清除,而通过这样的尾调用优化函数的递归,可以使得空间复杂度为O(1),不会导致爆栈的情况出现
-
// 递归执行改用同一个函数循环执行,只不过是改变了参数去调用(最多同时两个函数上下文)
function tco(func) {
var active = false;
var arg = [];
var re_value;
return function () {
// 记录每一次执行func函数的参数
arg.push(arguments);
// 仅有第一次调用该函数可以进入
if (!active) {
active = true;
// 通过循环执行同一个函数
while(arg.length > 0) {
re_value = func.apply(this,arg.shift());
}
active = false;
return re_value;
}
// 其它直接调用结束,销毁上下文
};
}
var tailCallSum = tco(function (total,count) {
if (count === 0) return total;
else {
return tailCallSum(total + 1,count - 1);
}
});
console.log(tailCallSum(0,1000000));
复制代码