admin管理员组文章数量:1588140
JS
1、原型链
实例对象的constructor也会指向构造函数
因为没有constructor属性会通过原型链找(容易忽略,是个小陷阱)
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
__proto__
来自于 Object.prototype,更像是一个 getter/setter,使用 obj.__proto__
时,可以理解成返回了 Object.getPrototypeOf(obj)
2、继承
原型链继承:子函数的原型是父函数的实例对象。
缺点不能传参,引用属性共享
构造函数继承:子函数中通过call调用父函数,改变this
缺点:每次都要调用父函数
组合继承缺点:调用两次父构造函数
一次是设置子类型实例的原型的时候:
Child.prototype = new Parent();
一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18'); // 调用了Child中的Parent.call(this, name);
3、作用域链
新版ES2018中规定执行上下文包含了:
词法环境(这就是旧版的作用域链和this合在一起)
变量环境
…其他
[[scope]]中保存了当前函数的作用域链,这个属性无法访问,属于内部属性
函数执行上下文中,作用域链 和 变量对象 的创建过程
简单栗子:
var scope = "global scope"
function checkscope(){
var scope2 = 'local scope'
return scope2
}
checkscope()
执行过程,伪代码:
1)函数创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO //有全局环境
]
2)执行上下文压入执行栈
ECStack = [
checkscopeContext, //压入栈
globalContext
]
3)执行上下文初始化:
上下文对象复制函数的[[scope]]属性创建作用域链
checkscopeContext = {
//创建上下文
Scope: checkscope.[[scope]],
this: undefined,
}
用 arguments 创建活动对象AO,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
//创建这个对象
arguments: {
length: 0
},
scope2: undefined,
},
Scope: checkscope.[[scope]],
this: undefined,
}
将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]], // 压入栈
this: undefined,
}
4)执行函数:修改AO的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope' // 修改这里
},
Scope: [AO, [[Scope]]],
this: undefined,
}
5)函数返回后,执行上下文从栈中弹出
ECStack = [
globalContext // 只剩全局上下文
];
4、闭包
MDN
闭包定义:闭能够访问自由变量的函数
自由变量:在函数中使用的,但既不是函数参数也不是函数的局部变量的变量(就是上层上下文中的变量)
定义:
1)从理论角度:所有的函数。因为创建的时候就讲上层上下文的数据保存,并可以引用
2)从实践角度:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
通过IIFE创建了函数上下文
data[0]
执行函数时,作用域链多了一层
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
能找到i的值,就不会再去全局上下文找,所以值是对的
5、变量对象
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
在全局上下文中,全局对象就是变量对象
只有当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以叫activation object
执行上下文的代码分成两个阶段:
1)进入执行上下文初始化
变量对象包括:
- 函数的所有形参 (如果是函数上下文)
- 函数声明,后声明的会覆盖之前的
- 变量声明,不会干扰已存在的同名形参或者函数名
简单栗子
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){
},
}
2)执行代码
根据代码修改AO中的值
6、this
ECMAScript的类型分为两种:语言类型、规范类型
语言类型 就是7种基本类型:string,number,bigint,boolean,null,undefined,symbol 和一种引用类型:obj
规范类型 用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型,用来描述语言底层行为逻辑。包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。
Reference
定义: 用来解释诸如 delete、typeof 以及赋值等操作行为
三部分组成:
- base value (属性所在的对象或者是EnvironmentRecord,值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种)
- referenced name (属性名称)
- strict reference (是否是严格引用)
Reference 组成部分的方法,比如 GetBase 和 IsPropertyReference。
两个组成部分的方法
1.GetBase
返回 reference 的 base value
2.IsPropertyReference
简单的理解:如果 base value 是一个对象,就返回true。
GetValue:用于从 Reference 类型获取对应值的方法
调用 GetValue,返回的将是具体的值,而不再是一个 Reference
如何确定this的值
步骤:
1.计算 MemberExpression 的结果赋值给 ref
2.判断 ref 是不是一个 Reference 类型
2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
ImplicitThisValue 该方法始终返回 undefined
2.3 如果 ref 不是 Reference,那么 this 的值为 undefined
什么是 MemberExpression ?
- PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》
- FunctionExpressio // 函数定义表达式
- MemberExpression [ Expression ] // 属性访问表达式
- MemberExpression . IdentifierName // 属性访问表达式
- new MemberExpression Arguments // 对象创建表达式
说白了就是比如 foo.bar()、foo[0]、foo.obj 这些运算中,括号、点运算符、中括号运算符之前的表达式要先进行计算,为 null 或者其他不能用的情况就会报错。
几种调用情况下的this
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
foo.bar()
1、计算 MemberExpression 的结果 赋值给 ref 如下:
var ref = {
base: foo,
name: 'bar',
strict: false
};
2、IsPropertyReference(ref) 由于 ref.base 是 foo,所以返回 true
3、执行 GetBase(ref) 返回 foo, 赋值给 this
------------------------------------------------
(foo.bar)()
1、括号没有对 foo.bar 做任何计算,所以结果同上
------------------------------------------------
(foo.bar = foo.bar)()
1、赋值计算调用了 GetValue, 返回的不再是 Reference 类型, this 为 undefined
------------------------------------------------
(false || foo.bar)()
同上,调用了 GetValue
------------------------------------------------
(foo.bar, foo.bar)()
同上,调用了 GetValue
------------------------------------------------
foo()
1、计算 MemberExpression 的结果 赋值给 ref 如下:
var ref = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
2、base value 是 EnvironmentRecord, this 的值为 ImplicitThisValue(ref), 返回 undefined
上述情况是从规范的角度去理解 this,大部分人是从调用的角度去理解,但是这个角度会无法去理解为何 (false || foo.bar)() 这种情况的 this 值
7、立即执行函数表达式(IIFE)
先看一组比较:
function foo(){
}() 报错,js解析器会当成函数声明
var foo = function(){
console.log(1)}() 可以执行
function foo(){
}(1) 不会报错,等同于下面的代码
function foo(){
}
(1)
在 js 里圆括号中不能包含声明,所以一般使用此方法将函数声明变成表达式
用类似 JQ 的返回对象来做私有变量会更好点,也是早期的模块化
8、instanceof 和 typeof 的实现原理
js 如何存储数据类型信息
js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息
- 000:对象
- 010:浮点数
- 100:字符串
- 110:布尔
- 1:整数
两个特殊值:
null:所有机器码均为0
undefined:用 −2^30 整数来表示
所以 typeof 判断 null 为对象,机器码低位相同
instanceof 原理:右边变量的 prototype 在左边变量的原型链上
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__ // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
9、bind
特点:
1)返回函数
2)传参2次:调用bind的时候可以传参,返回的新函数调用时也可以传参 3)绑定之后返回的新函数,作为构造函数时,绑定的this应该失效
具体实现
Function.prototype.bind2 = function (context) {
let self = this;
let args = [...arguments].slice(1) // 拿到第一次调用时,除了上下文之外的其他参数
let fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments); // 获取第二次调用的参数
// 第三个特点,如果是构造函数调用,绑定这个构造函数的实例为 this, 否则是我们传的上下文
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
// 将被绑定函数的原型 放到 返回函数的原型链上,
// 通过空函数中转,防止修改一个影响另一个
let fNOP = function () {
}
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()
return fBound; // 第一个特点,返回函数
}
10、call 和 apply
第一个参数指定为 null 或 undefined 时会自动替换为指向全局对象
call 的实现
Function.prototype.call = function (thisArg) {
// 先判断当前的甲方是不是一个函数(this就是Product,判断Product是不是一个函数)
if (typeof this !== 'function') {
throw new TypeError('当前调用call方法的不是函数!')
}
// 保存甲方给的参数
const args = [...arguments].slice(1)
// 传入的是 null 或者 undefined
thisArg = thisArg || window
// 将调用call的函数保存为乙方的一个属性,为了保证不与乙方中的key键名重复使用Symbol
const fn = Symbol('fn')
thisArg[fn] = this
// 执行保存的函数,这个时候作用域就是在乙方的对象的作用域下执行,改变的this的指向
const result = thisArg[fn](...args)
// 执行完删除刚才新增的属性值
delete thisArg[fn]
// 返回执行结果
return result
}
apply 的实现
Function.prototype.appy= function (thisArg) {
if (typeof this !== 'function') {
throw new TypeError('当前调用apply方法的不是函数!')
}
// 此处与call有区别,因为只有2个参数,其他一样
const args = arguments[1]
thisArg = thisArg || window
const fn = Symbol('fn')
thisArg[fn] = this
const result = thisArg[fn](...args)
delete thisArg[fn]
return result
}
11、柯里化
Function.length 表示形参的个数,不包括剩余参数个数,同时只计算第一个有默认值之前的参数
柯里化(Curry):一个函数接收一个多参函数,并且返回多个嵌套的只接受一个参数的函数
简单栗子:
fn(1)(2)(3)
偏函数应用(Partial Application):每个嵌套的函数可以接受不止一个参数
简单栗子:
fn(1,2)(3)
实现(不考虑占位符)
占位符根据多种不同情况用 if-else 处理,用一个数组保存占位符在总的参数列表中的位置,然后替换
function curry(targetFn) {
return function curried(...args) {
// 如果参数个数 达到 目标函数所需的参数,执行目标函数
if (args.length >= targetFn.length) {
return targetFn.apply(this, args)
} else {
// 否则递归柯里化函数:将上次递归抛出的函数获得的参数 args2,和以前累计的参数 args 传递给柯里化函数
return function(...args2) {
return curried.apply(this, [...args, ...args2])
}
}
}
}
12、垃圾回收
v8引擎的内存限制
V8引擎在64位系统下最多只能使用约1.4GB的内存,在32位系统下最多只能使用约0.7GB的内存。
原因:
1)浏览器端很少需要操作太多内存资源的场景
2)JS 单线程机制
没有复杂的多线程执行场景,对程序内存要求低
3)垃圾回收机制
垃圾回收耗时久。假设V8的堆内存为1.5G,那么V8做一次小的垃圾回收需要50ms以上,而做一次非增量式回收甚至需要1s以上。内存使用过高,必然垃圾回收时间变长,主线程等待时间也变长。
node 中可以手动设置内存最大与最小值
设置新生代内存中单个半空间的内存最小值,单位MB
node --min-semi-space-size=1024 xxx.js
设置新生代内存中单个半空间的内存最大值,单位MB
node --max-semi-space-size=1024 xxx.js
设置老生代内存最大值,单位MB
node --max-old-space-size=2048 xxx.js
查看当前node进程所占用的实际内存
heapTotal:V8 当前申请到的堆内存总大小。
heapUsed:当前内存使用量。
external:V8 内部的 C++ 对象所占用的内存。
rss(resident set size):表示驻留集大小,是给这个node进程分配了多少物理内存,这些物理内存中包含堆,栈和代码片段。
对象,闭包等存于堆内存,变量存于栈内存,实际的JavaScript源代码存于代码段内存
使用 Worker 线程时,rss 也包括 Worker 线程的值,但其他的值只针对当前线程
垃圾回收策略
总结:基于分代式垃圾回收机制,根据对象的存活时间将内存进行不同的分代,然后采用不同的垃圾回收算法
V8的内存结构
分为几个部分:
新生代(new_space)
:大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁。该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。老生代(old_space)
:新生代中的对象在存活一段时间后就会被转移到老生代内存区,垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。大对象区(large_object_space)
:存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。代码区(code_space)
:代码对象,会被分配在这里,唯一拥有执行权限的内存区域。map区(map_space)
:存放Cell和Map,每个区域都是存放相同大小的元素,结构简单
新生代
构成:两个 semispace (半空间)
使用算法:Scavenge算法,牺牲空间换时间。老生代内存生命周期长,可能会存储大量对象,不适用这种算法
具体实现使用了 Cheney 算法。
1、激活状态的区域叫做 From 空间,垃圾回收时把 From 空间中不能回收的对象复制到 To 空间
2、清除 From 中所有的非存活对象,两个空间呼唤身份
缺点:浪费空间,一半的内存用于复制
反思:为什么不标记完直接清除,而使用 Scavenge ,应该也是为了整理内存碎片
对象晋升
两个条件满足其一:
- 对象是否经历过一次Scavenge算法
- To空间的内存占比是否已经超过25%(防止变成 From 空间后,后续对象内存分配时内存过高溢出)
老生代
使用算法:Mark-Sweep (标记清除) 和 Mark-Compact (标记整理)
总步骤:标记、整理、清除
1)Mark-Sweep (标记清除)
详细步骤:
- 垃圾回收器在内部构建一个根列表, 保存所有的根节点
- 从所有根节点出发,遍历其可以访问到的子节点,标记为活动的
- 释放所有非活动的内存块
根节点类型
- 全局对象
- 本地函数的局部变量和参数
- 当前嵌套调用链上的其他函数的变量和参数
问题
一次标记清除后,内存空间可能会出现不连续的状态-----内存碎片
后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,所以需要 标记整理
2)Mark-Compact (标记整理)
详细步骤:
- 将所有活动对象往堆内存的一端移动
3)性能提升
全停顿
:由于 JS 是单线程的,垃圾回收的过程会阻塞主线程同步任务
增量标记
:标记、交给主线程、回到标记暂停的地方继续标记
如果在老生代中,对堆内存中所有的存活对象遍历,势必会造成性能问题。
于是 V8 引擎先标记内存中的一部分对象,然后暂停,将执行权重新交给 JS 主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。
挺像使用 setTimeout 优化技巧,也是把一个大的任务拆成很多个小任务,这样就可以间断性的渲染 UI,不会有卡顿的感觉
基于增量标记, V8 引擎后续继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction)、并行标记、并行清理
如何避免内存泄漏
避免使用全局变量
:因为 window 对象可以作为根节点,上面的属性都是常驻的
手动清除定时器
少用闭包
清除DOM引用
:对保存在属性中的 dom 引用及时释放成 null
使用弱引用
:WeakMap 和 WeakSet 中的引用都是弱引用,只要对象没有其他的引用,这个对象中所有属性的内存都会被释放掉
13、浮点数精度
数字类型
Number 类型使用 IEEE 二进制浮点数算术标准 中的 双精度64位表示法,也就是64位字节存储一个浮点数
浮点数转二进制
浮点数 (Value) 可以这样表示
Value = sign * exponent * fraction
1)1 位存储 S,0 表示正数,1 表示负数。
2)11 位存储 E(阶码) + bias,对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。
最大值是1024,因为E可能为1,所以bias的值是固定的1023,存储的时候通过存储的二进制值减去1023反推得到E的值。
3)52 位存储 Fraction。
0.1 对应的二进制
Sign 是 0,E + bias 是 -4 + 1023 = 1019,1019 用二进制表示是 1111111011,Fraction是1001100110011…(下方位1.不用存,是固定的)
1 * 1.1001100110011…… * 2^-4
64字节位表示
0 01111111011 1001100110011001100110011001100110011001100110011010
0.2 对应的 64 字节
0 01111111100 1001100110011001100110011001100110011001100110011010
浮点数的运算
例如:0.1 + 0.2
1)对阶
把阶码调整为相同
0.1 是 1.1001100110011…… * 2^-4,阶码是 -4
0.2 是 1.10011001100110…* 2^-3,阶码是 -3
小阶对大阶:0.1 的 -4 调整为 -3, 数字会变大,所以前面的应该变小,也就是右移,符号位补0
2)尾数运算
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
———————————————————————————————————————————————————
10.0110011001100110011001100110011001100110011001100111
结果:10.0110011001100110011001100110011001100110011001100111 * 2^-3
3)规格化
移一位:1.0011001100110011001100110011001100110011001100110011(1) * 2^-2
4)舍入处理(0 舍 1 入)
括号里的1是多出来的,会舍弃,并进1
5)溢出判断(这里没有)
6)结果
0 01111111101 0011001100110011001100110011001100110011001100110100
十进制就是 0.30000000000000004440892098500626
由于两次存储时的精度丢失,再加上运算时的精度丢失,导致了这个结果
扩展:为什么(2.55).toFixed(1)等于2.5?
简单总结:2.55的存储要比实际存储小一点,导致0.05的第1位尾数不是1,所以就被舍掉了
14、new
特点:
1)返回的对象,可以访问传入的构造函数里的属性
2)返回的对象,可以访问传入的构造函数 原型 里的属性
3)判断构造函数是否有返回值,如果是对象就返回对象,不是的话就返回我们创建的
实现(使用一个函数模拟)
function objectFactory() {
var obj = new Object(),
Constructor = [].shift.call(arguments); // 拿到传入的构造函数
obj.__proto__ = Constructor.prototype; // 创造的实例对象 连接 构造函数的prototype
var ret = Constructor.apply(obj, arguments); // 应用剩余的传入参数,this 改为创造的实例对象
return typeof ret === 'object' ? ret : obj; // 判断返回值
};
15、事件循环
特点:
当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。
同一次事件循环中,微任务永远在宏任务之前执行。、
node环境
node 选择 chrome v8 引擎作为js解释器,v8 引擎将 js 代码分析后去调用对应的 node api,而这些 api 最后则由 libuv 引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。
实际上node中的事件循环存在于libuv引擎中
poll 阶段
1)先查看 poll queue 中是否有事件
2)当 poll queue 为空时,检查是否有 setImmediate() 的 callback,进入 check 阶段
3)同时检查是否有到期的 timer,按照调用顺序放到timer queue中,进入 timer 阶段
4)2、3步顺序不一定,看具体的代码环境。
5)如果两者的 queue 都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入 i/o callback 阶段并立即执行这个事件的 callback
check 阶段 和 timer 阶段
check 阶段专门用来执行 setImmediate() 方法的回调,当 poll 阶段进入空闲状态进入
timer 阶段执行 setTimeout 或者 setInterval 函数的回调
I/O callback阶段
执行大部分I/O事件的回调,包括一些为操作系统执行的回调。
例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。
close阶段
当一个 socket 连接或者一个 handle 被突然关闭时(例如调用了 socket.destroy() 方法),close 事件会被发送到这个阶段执行回调。否则事件会用 process.nextTick()方法发送出去。
process.nextTick
node中存在着一个特殊的队列,即nextTick queue
当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列,且不会停止,所以可能造成内存泄漏。
setTimeout 与 setImmediate 的区别与使用场景
在在定时器回调或者 I/O 事件的回调中,setImmediate 方法的回调永远在 timer 的回调前执行。
其他场景取决于当时机器情况
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log
本文标签: 笔记
版权声明:本文标题:2022前端笔记 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dongtai/1728025902a1142665.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论