原始值和引用值
栈跟堆的特点
- 栈
- 先进后出
- 内存分配连续且自动管理
- 访问速度快
- 空间比较小
- 堆
- 空间大
- 内存不是连续的,管理依赖垃圾回收机制
- 访问速度相对于栈比较慢
原始值类型存在7种
- Number(数字):表示数值,包括整数和浮点数。
- String(字符串):表示文本数据,使用引号括起来。
- Boolean(布尔值):表示逻辑值,即true或者false.
- Null(空):表示一个空值或没有值的对象。
- Undefined(未定义):表示一个未被赋值的变量的值
- Symbol(唯一值符号):表示唯一的标识符。
- bigint:表示任意精度的整数,用 n作为字面量后缀
js
// number
const a = 1
// string
const b = 'zifuch'
引用值类型
-
对象,对象包括为数组对象,日期对象,函数对象,等等等,一切皆对象
jsconst obj = { a:1, b:2 } const arr = ['1','2','3']
两者存值的方式
-
原始值存储在栈中
jslet a = 1; let b = 2;
-
引用值存储在推中
jsconst a = { b:1, c:2 }
复制值
-
原始值复制
jslet a = 1; let b = a; b = 2; console.log("a的值是:",a); console.log("b的值是:",b);
-
引用值复制
jsconst a = { b:1, c:2, } const copyA = a; copy.c = 5; console.log("a.c的值是:",a.c);
-
引用值复制2
iniconst a = { b:1, c:2, } const copyA = a; copyA = { b: 3, c: 4, } copy.c = 5; console.log("a.c的值是:",a.c);
浅拷贝和深拷贝
-
浅拷贝
浅拷贝是只复制对象的第一层属性,如果属性是引用类型(对象、数组等),拷贝的是引用地址,而不是新对象。
- object.assign()
- 扩展运行符...
jsconst obj1 = { a: 1, b: { c: 2 } }; const obj2 = Object.assign({}, obj1); obj2.a = 100; // 不影响 obj1 obj2.b.c = 200; // 影响 obj1,因为 b 是引用 console.log(obj1.b.c); // 200(共享 b 对象)
-
深拷贝
深拷贝会递归复制对象的所有层级属性,确保拷贝后的对象与原对象完全独立。
- JSON.parse(JSON.stringify(...))
- structuredClone()浏览器内置API
- Lodash工具库
垃圾回收机制
JS的垃圾回收机制(Garbage Collection,简称GC)帮我们自动管理内存,对于不再使用的对象占用的内存进行释放。
- Garbage : 指不再被引用、不可达的内存对象。
- Collection: 指 JS 引擎自动检测并释放这些无用内存的过程。 常见的垃圾回收机制有两种,引用计数 和标记清除。
引用计数
每个对象都有一个计数器,记录当前有多少个引用指向它。
规则
- 声明变量并给它赋一个引用值的时候,值的引用数+1
- 类似的把同一个值又被赋值给另外一个变量,值的引用数也+1
- 保存对该引用的变量被其它值给覆盖的时候,值的引用数-1
- 当引用解除(比如赋值为null)时,值的引用数-1
js
let obj = { name: "JS" }; // 引用计数 +1
let a = obj; // 引用计数 +1
obj = null; // 引用计数 -1 (现在还剩 a 引用)
b = null; // 引用计数 -1 (没有引用了,计数为 0 → 可回收)
缺点
当出现循环引用的情况出现的时候,引用计数永远不会清0。
js
function problem() {
let objectA = new Object(); // A对象引用计数 +1
let objectB = new Object(); // B对象引用计数 +1
objectA.someOtherObject = objectB; // B对象引用计数 +1
objectB.anotherObject = objectA; // A对象引用计数 +1
}
problem();
那有人说,我都置为null不就可以了,比如像下这样
js
function problem() {
let objectA = new Object(); // A对象引用计数 +1
let objectB = new Object(); // B对象引用计数 +1
objectA.someOtherObject = objectB; // B对象引用计数 +1
objectB.anotherObject = objectA; // A对象引用计数 +1
objectA.someOtherObject = null;
objectB.anotherObject = null;
let objectA = null;
let objectB = null;
}
problem();
但是现实开发中的循环引用会复杂很多,开发者很难自己追踪到里面的关系,容易造成内存泄漏。so,它被淘汰了。
标记清除
标记清除是JavaScript中最常用的垃圾回收机制,特别是在V8引擎之中,而其核心思想是可达性(Reachability)
规则
标记阶段: 垃圾回收器从根对象(通常是全局对象、活动执行栈和闭包等)出发,遍历所有可访问的对象,并标记为活动对象。在这个阶段,垃圾回收器会识别出所有被引用的对象,将其标记为"存活"。
清除阶段: 在标记阶段完成后,垃圾回收器会对堆内存进行扫描,清除所有未标记的对象,这些对象被认为是"垃圾",因为它们不再被任何活动对象引用。清除阶段会释放这些垃圾对象所占用的内存空间,使其可用于未来的对象分配。

作用域
概念 : js代码在查找变量时的一个范围。
词法作用域
作用域在代码编写时就已经确定下来,并且由函数声明的位置决定,而不是由函数调用的位置决定。
js
var name = "global";
function foo() {
console.log(name);
}
function bar() {
var name = "bar";
foo();
}
bar(); // 输出global
JS的三个作用域
- 全局作用域: 对浏览器来说全局的作用域的话就是挂载window上的变量,或者是顶层用var声明的变量。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
- 函数作用域: 在函数执行的时候,函数内部的变量具有函数作用域。
- 块级作用域: 嵌套在{}括号里面的变量(var声明的变量不具备块级作用域)
执行上下文和执行上下文栈
执行上下文(上下文) :变量或函数的上下文决定他们能访问到哪些数据。每个上下文都有一个关联的词法环境(Lexical Environment)\变量对象(variable object)。
执行上下文栈:当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。
js
let a = 1
function f1() {
const b = 2;
function f2 () {
const c = 3;
}
f2();
}
f1();

作用域链
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain),指向上一个作用域。这个作用域链决定了当前上下文中的代码在访问变量和函数时的顺序
js
let a = 1
function f1() {
const b = 2;
function f2 () {
const c = 3;
}
f2();
}
f1();
例子
js
var a = 10;
var b = 10;
var c = 10;
function foo() {
var b = 20;
function bar() {
var c = 30;
console.log(a, b, c,d);
}
bar();
}
foo();
注意点:
- 函数的参数属于当前函数上下文。
- 如果内部作用域和外部作用域使用相同的变量名,则直接使用离当前作用域近的变量名,有书籍称为遮蔽。
分享文章链接
- juejin.cn/post/721142... 作者对《你不知道的JS》第一部分第二部分总结的挺好。
- github.com/getify/You-... 《你不知道的js》原文
闭包
概念:闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套中实现的。(即函数内部嵌套函数,内部函数使用了外部函数作用域变量)
js
function outer() {
let count = 0; // outer 的局部变量
function inner() {
count++;
console.log(count);
}
return inner;
}
const fn = outer(); // outer 执行结束,但 count 没被销毁
fn(); // 1
fn(); // 2
fn(); // 3
为了帮助理解闭包,举了以下三个情况,希望对你有帮助。
-
情况1
jsfunction outer() { let count = 0; // outer 的局部变量 function inner() { count++; console.log(count); } return inner; } // 定义了函数 函数没有执行 可以理解为闭包的结构体有了 但是闭包不存在
-
情况2
jsfunction outer() { let count = 0; // outer 的局部变量 function inner() { count++; console.log(count); } return inner; } outer(); // 闭包产生了 但是闭包不会持久存在,因为没有保留对它的引用
-
情况3
jsfunction outer() { let count = 0; // outer 的局部变量 function inner() { count++; console.log(count); } return inner; } fn = outer(); // 闭包产生了 fn保留了对它的引用 持久存在
this
概念:this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值。
默认绑定
全局上下文中调用函数的this指向window
js
window.color = 'red'
function fn() {
consoel.log("color:",this.color)
}
fn(); // color: 'red'
隐式绑定
函数作为对象的方法调用,this 绑定到该对象
csharp
let obj = {
color: 'blue'
fn: fn;
}
function fn () {
console.log("color:",this.color);
}
obj.fn(); // color: 'blue'
显示绑定
使用call、apply、bind显示指定this
js
function fn() {
console.log("color:",this.color);
}
const obj = { obj: 'yellow' };
greet.call(obj); // color: 'yellow'
new关键字绑定
构造函数被new调用时,this指向新创建的对象
js
function Color (color) {
this.color = color;
}
const c = new Color("yellow");
console.log(c.color); // 'yellow'
注意
- 箭头函数不绑定自己的this,它会捕获定义是的外层作用域中的this
js
window.color = 'red';
let obj = {
color: 'green';
fn: () => {
console.log('color:',this.color);
}
}
obj.fn(); // color: 'red'
- 绑定的优先级
new绑定>显示绑定>隐式绑定>默认绑定
js
// 以下是显示绑定和隐式绑定对比的列子
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
原型和原型链
JS是一门面向对象语言,那么面向对象语言的特点是封装、继承、多态。在C++或者Java里面都是由类去实现,但JS没有类的说法,在JS里面是由原型实现的,ES6才拥有了类,但本质上是原型的语法糖。
构造函数 :构造函数是用于创建特定类型对象/实例的一种方法。默认首字母大写 。
new关键字:用于创建构造函数的对象。
在c++里面我们创建同一类型的对象一般是这么写
c++
#include <iostream>
using namespace std;
class Animal {
public:
// 成员变量
string name;
int age;
// 构造函数
Animal(string n, int a) {
name = n;
age = a;
}
// 成员方法:打印动物信息
void printInfo() {
cout << "Animal Name: " << name << ", Age: " << age << endl;
}
};
int main() {
// 创建对象
Animal dog("Buddy", 3);
Animal cat("Kitty", 2);
// 调用方法
dog.printInfo(); // Animal Name: Buddy, Age: 3
cat.printInfo(); // Animal Name: Kitty, Age: 2
return 0;
}
在js我们不使用原型的话大概是这么写
js
// 构造函数
function Animal(name, age) {
this.name = name;
this.age = age;
// 方法直接在构造函数中定义
this.printInfo = function() {
console.log(`Animal Name: ${this.name}, Age: ${this.age}`);
};
}
// 创建对象
const dog = new Animal("Buddy", 3);
const cat = new Animal("Kitty", 2);
// 调用方法
dog.printInfo(); // Animal Name: Buddy, Age: 3
cat.printInfo(); // Animal Name: Kitty, Age: 2
这么写的缺点是:每new一次,就会在内存里重新创建一个新的函数对象,虽然它两长的一模一样,但他们指向的函数不一样,会导致内存浪费。
js
dog.prinrInfo === cat.printInfo // false
// 插播: == 不比较类型 === 需要比较类型
// 1 == '1' √ 1 === '1' ×
使用原型进行改善
js
// 构造函数
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.pritInfo = function {
console.log(`Animal Name: ${this.name}, Age: ${this.age}`);
}
// 创建对象
const dog = new Animal("Buddy", 3);
const cat = new Animal("Kitty", 2);
// 调用方法
dog.printInfo(); // Animal Name: Buddy, Age: 3
cat.printInfo(); // Animal Name: Kitty, Age: 2
// dog.printInfo === cat.printInfo;
接下来,进入正文...
显示原型
每一个函数身上都会创建一个prototype属性,函数原型是一个对象。在它上面定义的属性和方法可以被对象实例共享。
js
function Animal () {
//.....
}
Animal.prototype.name = '猫'
const animal = new Animal();
console.log(animal.name); // '猫'

constructor
原型对象身上存在一个默认的constructor属性,指回与之关联的构造函数。
js
Animal.prototype.constructor === Animal

隐式原型
每次调用构造函数创建一个新实例,这个实例的内部 [[Prototype]] 指针 就会被赋值为构造函数的原型对象。
在现代化浏览器如Firefox、Safari 和 Chrome 会在每个对象上暴露 "proto" 属性,通过这个属性可以访问对象的原型。
js
function Animal () {
//.....
}
let animal = new Animal();
animal.__proto__ === Animal.prototype;

原型链
构造函数的原型是一个对象,那么会存在这么一个关系
js
Animal.prototype.__proto__ === Object.prototype

那么Object原型的原型是什么?为null。正常的原型链都会终止于 Object 的原型对象。
js
Animal.prototype.__proto__.__proto__ === null

那么构造函数也是一个对象,由Function构造函数创建,Function构造函数由也是由本身创建。

总结 :
当我们通过对象访问它的属性的时候,会按照这个属性的名称开始搜索。先搜索对象实例本身,如果对象实例本身没有找到就会沿着隐式原型__proto__上的原型对象上查找,直至找到原型链的终端null为止。
为什么是隐式原型链?实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。