跳到主要内容

变量, 作用域和内存

原始值和引用值

primitive values

primitive values
  • 原子数据类型;
    • undefined;
    • null;
    • boolean;
    • number;
    • string;
    • symbol;
操作机制
  • 变量存储栈中 primitive values 的内存地址;
  • primitive value 直接存储在栈中;
  • 直接操作存储在栈里面的实际值;

reference values

reference values
  • 引用数据类型;
    • object;
操作机制
  • 变量存储栈中 reference values 的内存地址;
  • 栈中存储 reference value 在堆中的内存地址;
  • 操作对 object 的引用,并不直接操作 object 本身;

动态属性

// primitive values 无属性;
// reference values 有属性;
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"

复制值

primitive value
  • 在栈中创建一个新空间;
  • 然后将旧变量在栈中的实际值赋值给新空间;
  • 将新空间栈中的内存地址赋值给新变量;
  • 两者相互隔绝;

值传递

reference value
  • 在栈中创建一个新空间;
  • 然后将旧变量在栈中的实际值赋值给新空间;
  • 将新空间栈中的内存地址赋值给新变量;
  • 但新变量在栈中的对应值是指向对象的堆内存地址;
  • 两者实际还是指向一个对象;
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"

引用传递

传递参数

机制
  • JavaScript 中的函数参数全是值传递,将其一个副本传递给函数内部;
  • 对于 primitive value;
    • 传递其 value 的副本;
    • 函数内部的改变不会影响函数外部;
  • 对于 reference value;
    • 传递其 value 副本;
    • 但 reference value 是指向 object 的指针;
    • 函数内部的改变会影响函数外部;
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

typeof 和 instanceof

typeof 操作符

作用
// 检测 reference value 和 primitive value 数据类型
let message = "some string";
console.log(typeof message); // "string"
返回值
检测对象返回值
undefined"undefined"
Boolean"boolean"
string"string"
number"number"
object or null"object"
function"function"
symbol"symbol"
function
  • ECMA-262 规定具有 call 方法的对象返回 function;
  • 正则表达式同样具有 call,故也返回 function;

instanceof 操作符

作用
  • 检测 reference value 具体数据类型;
语法格式
// 判断 variable 是否是 constructor 的实例
console.log(person instanceof Object); // is the variable person an Object?
console.log(colors instanceof Array); // is the variable colors an Array?
console.log(pattern instanceof RegExp); // is the variable pattern a RegExp?
variable instanceof object
  • 检测 reference value,总是 true;
  • 检测 primitive value,总是 false;

执行上下文

词法环境

lexicalEnvironment
  • 定义标识符与变量,函数等类型的关联;
lexicalEnvironment 的组成
  • Environment Record:存储标识符与变量,函数等类型的关联;
  • outer:指向的上级 lexicalEnvironment;
  • this:lexicalEnvironment 绑定的 this;
Environment Record 的分类
  • Declarative Environment Record;
    • Function Environment Record:对应于函数执行上下文;
    • Module Environment Record:对应与 ESM 模块的顶级上下文,outer 为 Global Environment Record;
  • Object Environment Record:对应于 with 创建的执行上下文;
  • Global Environment Record:对应于全局上下文;
this 的确定
  • 全局执行上下文;
    • 严格模式为 undefined;
    • 非严格模式为 windows/global;
  • 函数执行上下文;
    • 普通函数:根据函数被调用时的方式;
      • 函数被对象调用/new,this 指向该对象 (对象/构造函数/类实例);
      • 函数不被对象调用,this 指向 undefined/windows;
    • 箭头函数;
      • 箭头函数创建的执行上下文的 lexicalEnvironment 无 this binding;
      • 其沿着 lexicalEnvironment 中 outer 获得最邻近 this,使用该 this;
// 普通函数
function test() {
console.log(this);
}
const o = {
test: test;
};
// 普通函数不被被对象调用, this 指向该对象
test(); // windows
// 普通函数被对象 o 调用, this 指向 o
o.test(); // Object: o

// 箭头函数
const o = {
test0: () => {
console.log(this);
};
test1: function () {
(() => {
console.log(this);
})();
};
};
// 箭头函数 test0 最邻近的lexicalEnvironment为全局上下文的 LexicalEnvironment, this 指向 undefined/windows
o.test0();
// test1 中的匿名箭头函数最邻近的lexicalEnvironment为 test1 的生成的函数执行上下文的 LexicalEnvironment, this 指向 test1 的 this, 即对象 o
o.test1();

执行上下文

执行上下文
  • 全局执行上下文:默认执行上下文;
  • 函数执行上下文:调用函数时创建一个全新的执行上下文;
执行上下文的组成
  • LexicalEnvironment:存储 let/const/function 等对应标识符的 lexicalEnvironment;
  • VariableEnvironment:存储 var 对应标识符的 lexicalEnvironment;
执行栈
  • 使用栈存储代码执行期间创建的执行上下文;
  • 程序运行创建全局执行上下文压入栈底;
  • 某一函数被调用时,创建一个新的函数执行上下文压入栈顶;
  • 函数调用完毕后移除栈顶;
  • 与作用域链相关;

执行栈

执行上下文的创建
  • 创建阶段;
    • 创建 LexicalEnvironment 和 VariableEnvironment;
    • 确定 this;
  • 执行阶段:完成变量赋值并执行代码;
// 示例代码
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);

// 全局上下文的创建
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object";
// Identifier bindings go here
a: < uninitialized > ;
b: < uninitialized > ;
multiply: < func >
}
outer: < null > ;
ThisBinding: < Global Object >
};
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object";
// Identifier bindings go here
c: undefined;
}
outer: < null > ;
ThisBinding: < Global Object >
}
}

// 全局上下文的执行
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object";
// Identifier bindings go here
a: 20;
b: 30;
multiply: < func >
}
outer: <null>;
ThisBinding: <Global Object>
};
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object";
// Identifier bindings go here
c: undefined;
}
outer: <null>;
ThisBinding: <Global Object>
}
}

// 函数执行上下文的创建
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative";
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2};
};
outer: <GlobalLexicalEnvironment>;
ThisBinding: <Global Object or undefined>;
};
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative";
// Identifier bindings go here
g: undefined
};
outer: <GlobalLexicalEnvironment>;
ThisBinding: <Global Object or undefined>
}
}

// 函数执行上下文的执行
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative";
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2};
};
outer: <GlobalLexicalEnvironment>;
ThisBinding: <Global Object or undefined>;
};
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative";
// Identifier bindings go here
g: 20
};
outer: <GlobalLexicalEnvironment>;
ThisBinding: <Global Object or undefined>
}
}
var 变量提升的本质
  • 执行上下文创建时;
  • var 声明的变量赋值为 undefined,let/const 声明的变量赋值为 uninitialized;
  • 所以 var 变量在对应语句声明之前便可以使用;
LexicalEnvironment 和 VariableEnvironment 区分的原因
  • 处理 ES6 引入的 let 和 const;
  • 当执行上下文中执行 时;
    • 首先保存当前执行上下文的 LexicalEnvironment 为 oldEnv;
    • 创建一个新 DeclarativeEnvironment 为 blockEnv;
      • 无 this binding;
      • outer 为 oldEnv;
    • 基于 block 语句,创建词法环境,将对应标识符添加至 LexicalEnvironment 或 VariableEnvironment;
    • 将当前执行上下文的 LexicalEnvironment 的 LexicalEnvironment 设置为 blockEnv;
    • 执行 block 中的语句,完成词法环境求值阶段;
    • block 执行完毕,移除 blockEnv,将当前执行上下文的 LexicalEnvironment 的 LexicalEnvironment 设置为 oldEnv;
  • 会生成一个临时的 LexicalEnvironment 也是 let/const/function 具有块级作用域的原因;
const a = 1;
{
const a = 2;
var test0 = function () {
console.log(this);
};
function test1() {
console.log(this);
}
// a 保存至 {} 创建的 LexicalEnvironment
console.log(a); // 2
}

// 在 {} 创建的 LexicalEnvironment 已经被移除并替换, 无法访问到
// 在全局执行上下文中的 LexicalEnvironment 中检索到 a
console.log(a); // 1

// 匿名函数被 var 声明 test0 保存至执行上下文中的 VariableEnvironment 中, 因此可以检索到, this 指向 undefined
console.log(test0()); // undefined
// test1 保存至 {} 临时创建的 LexicalEnvironment, 此时已经被移除并替换, 无法访问到
console.log(test1()); // ReferenceError: test1 is not defined

作用域和作用域链

作用域
  • var/const/let/function 等标识符可以访问的 lexicalEnvironment;
作用域分类
  • 全局作用域;
    • 存储在全局执行上下文的 LexicalEnvironment 或 VariableEnvironment 中;
    • 可以被执行栈上的任意执行上下文的 LexicalEnvironment 或 VariableEnvironment 中的任意标识符访问;
  • 函数作用域;
    • 存储在对应的函数执行上下文的 LexicalEnvironment 或 VariableEnvironment 中;
    • 可以被该函数执行上下文下级的执行上下文的 LexicalEnvironment 或 VariableEnvironment 中的标识符访问;
  • 块级作用域;
    • 存储在 临时创建的 LexicalEnvironment 中;
    • 仅能被该 LexicalEnvironment 中的标识符访问;
作用域链
  • 基于执行上下文 LexicalEnvironment 或 VariableEnvironment 中的 outer 访问执行栈中的变量和函数;
标识符查找机制
  • 首先为自身所在执行上下文;
    • 首先在 LexicalEnvironment 查找;
    • 然后在 VariableEnvironment 查找;
  • 其次为执行栈中的父级执行上下文,以此类推,直至到达 global context;

标识符查找机制

执行上下文增强

机制
  • 特定语句临时在执行上下文前增加上下文;
  • 代码结束后移除;
特定语句
  • try-catch 语句中的 catch 块;
  • with 语句,不推荐使用;
with 机制详解
// 对象 o 对应执行上下文在 buildUrl() 对应执行上下文后面;
// 利用 with 语句, 临时将其添加至 buildUrl() 对应执行上下文前面;
// 故可访问 buildUrl() 中的 qs 变量, 并在对象 o 中创建变量 url
// 代码执行完毕, 将其移除;
let o = { href: "kxh" };
function buildUrl() {
let qs = "?debug=true";
with (o) {
href = "2222";
var url = href + qs;
}
return url;
}
var result = buildUrl();
console.log(result); // 2222?debug=true

垃圾回收

垃圾回收
  • js 自动进行垃圾回收;
  • 即自动释放内存,释放不再使用的变量;

垃圾回收策略

内存对象
  • js 将内存对象分为新生代和老生代;
  • 新生代为寿命较短的对象;
  • 老生代为寿命较长的对象 (经过多次垃圾回收依旧存活);
停止-复制式垃圾回收
  • js 将堆分为两个空间,活动空间和空闲空间;
  • 垃圾回收过程中,暂停 js 运行,将存活内存对象从活动空间复制到空闲空间,对其整理和压缩;
  • 互换活动空间和空闲空间的内存对象;
  • 清理空闲空间内存对象,完成垃圾回收;
mark-and-sweep
  • mark:基于追踪垃圾回收算法,标记所有存活对象;
  • sweep:清理未被标记的对象;
mark-and-compact
  • mark:基于追踪垃圾回收算法,标记所有存活对象;
  • compact:压缩老生代内存对象,整理到内存一侧,避免内存碎片化;
增量标记算法
  • 垃圾回收时,暂停 js 代码运行,称为停顿;
  • 使用增量标记和并发标记技术,允许垃圾回收时,执行部分 js 代码;
    • 将标记过程分解为若干阶段;
    • 在执行 js 代码的同时完成标记;

新生代垃圾回收和老生代垃圾回收

  • 新生代垃圾回收;
    • 基于 Scavenge 算法进行垃圾回收,即停止-复制式垃圾回收;
    • 将新生代对象放入活动空间,活动空间满,触发垃圾回收;
  • 老生代垃圾回收:结合 mark-and-sweep 和 mark-compact;

circular reference 问题

  • A 和 B 循环引用,永远不会释放其内存;
  • 可通过手动赋值 null 解决;
function problem() {
let objectA = new Object();
let objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}

性能耗费

主要问题
  • garbage-collection 耗费较大;
  • 需要寻找一个好时机去清理垃圾;
  • 以防影响设备性能;

最快执行垃圾回收

  • 手动赋值 null;
  • 避免使用全局变量,定义在函数作用域中;
  • 使用 let 和 const 定义变量;
  • 减少闭包使用次数;
  • hidden class 机制;
    • v8 引擎在运行时创建 hidden classes 连接每个 object;
    • 共享同一个 hidden classes 的 object 具有更好的性能;
    • 优化原则;
      • 设计 object 时预先设计所有属性;
      • 初始化和删除属性时赋值 null;

最佳实践

this

示例一
  • fn() this 指向 global,global 是否具有 name;
    • 浏览器环境和 node 环境不一致;
      • var 声明的变量在浏览器端会定义在 windows 上,故输出 global;
      • var 声明的变量在 node 不会定义在 globalThis 上,故输出 undefined;
    • 如果不使用 var,直接声明,浏览器/node 端都会输出;
  • obj.say() this 指向 obj,this.name 为其名为 name 属性,返回 'kxh';
    • const 声明 name 定义在词法环境中,不是 obj 的属性,所以忽略;
// name = "this";
var name = "global";

const obj = {
name: "kxh",
say: function () {
const name = "la";
console.log(this.name);
},
};

const fn = obj.say;
fn(); // 浏览器输出 global, node 输出 undefined
obj.say();
示例二
  • obj1.intro1.call(obj2)();
    • 将 intro1 的 this 赋值给 obj2,输出 object2;
    • intro1 返回的箭头函数为定义时最临近的 this, 此时即 intro1 指向的 obj2,输出 object2;
  • obj1.intro1().call(obj2);
    • 首先执行 obj1.intro1(),将 intro1 的 this 赋值给 obj1,输出 object1;
    • 同 obj1.intro1.call(obj2)();
  • obj1.intro2.call(obj2)();
    • 但箭头函数的 this 指向定义时最临近的 this,此处为 windows;
    • intro2 返回的普通函数调用时无明确 this 指向,默认指向 windows;
  • obj1.intro2().call(obj2);
    • 同 obj1.intro2.call(obj2)();
    • intro2 返回的普通函数 this 明确为 obj2,故输出 object2;
var name = "window";
const obj1 = {
name: "obj1",
intro1: function () {
console.log(this.name);
return () => {
console.log(this.name);
};
},
intro2: () => {
console.log(this.name);
return function () {
console.log(this.name);
};
},
};
const obj2 = {
name: "obj2",
};
obj1.intro1.call(obj2)(); // object2 object2
obj1.intro1().call(obj2); // object1 object1
obj1.intro2.call(obj2)(); // window window
obj1.intro2().call(obj2); // window object2

手写 instanceOf

const customInstanceof = (origin, target) => {
let proto = Object.getPrototypeOf(origin);
while (proto != null) {
if (proto === target.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
};