2022前端笔记

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)进入执行上下文初始化

变量对象包括:

  1. 函数的所有形参 (如果是函数上下文)
  2. 函数声明,后声明的会覆盖之前的
  3. 变量声明,不会干扰已存在的同名形参或者函数名

简单栗子

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 (标记清除)

详细步骤:

  1. 垃圾回收器在内部构建一个根列表, 保存所有的根节点
  2. 从所有根节点出发,遍历其可以访问到的子节点,标记为活动的
  3. 释放所有非活动的内存块

根节点类型

  1. 全局对象
  2. 本地函数的局部变量和参数
  3. 当前嵌套调用链上的其他函数的变量和参数

问题

一次标记清除后,内存空间可能会出现不连续的状态-----内存碎片
后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,所以需要 标记整理

2)Mark-Compact (标记整理)

详细步骤:

  1. 将所有活动对象往堆内存的一端移动

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(() => 
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值