学习目标
学完本章你应该能够:
- 讲清 JS 的“值类型”和“引用类型”在内存上的本质区别,能预测一段赋值/修改代码的最终输出。
- 手写一个递归
deepCopy,正确处理数组、普通对象与嵌套结构,并说清为什么typeof obj !== "object" || obj === null要放在最前面。 - 用
class/extends/super写继承,并用instanceof判断原型链上的归属关系。 - 把“原型 / 原型链”讲成一张图:每个 class 有
prototype,每个实例有__proto__,属性查找沿着__proto__一路向上。 - 说清
this在不同调用场景(普通函数 / call·apply·bind / 对象方法 / 箭头函数 / class 方法)下到底取什么值,并能手写一个myBind。
前置知识:
- 任意一门编程语言基础(变量、函数、循环即可)
- 基本的内存概念(栈、堆,不要求深入)
- 浏览器里跑过最简单的 JS(
<script>或控制台)
本章你会动手做的事:
- 在控制台跑一遍“值类型 vs 引用类型”两段代码,亲眼看看改
b会不会影响a。 - 给
deepCopy传一个含数组和嵌套对象的对象,改拷贝后再打印原对象,验证“互不影响”。 - 写一个
myBind并测试:把普通函数的this绑到一个对象上,再验证this.name是否生效。
一、变量类型和计算
1. typeof 能判断哪些类型
1). 作用
- 判断所有值类型
// 判断所有值类型
let a;
typeof a; // 'undefined'
const str = "abc";
typeof str; // 'string'
const n = 100;
typeof n; // 'number’
const b = true;
typeof b; // 'boolean'
const s = Symbol("s");
typeof s; // 'symbol'
- 识别函数
// 能判断函数
typeof console.log // 'function'
typeof function () {}. // 'function'
- 判断是否是引用类型
// 能识别引用类型(不能再继续识别)
typeof null // 'object'
typeof l'a', 'b'] // 'object'
typeof { x: 100 } // 'object'
2. 值类型和引用类型
1). 值类型
let a = 50;
let b = a;
a = 100;
console.log(b); // 50
2). 引用类型
let a = { name: "wg" };
let b = a;
b.name = "zhangsan";
onsole.log(a.name); // zhangsan
3). 分类
// 值类型
undefine 字符串 数值 布尔 Symol
// 引用类型
obecjt{} 数组[] null
白话类比:值类型像“复印”——你把
a=50的纸条复印给b,之后你在a上改成 100,b那张复印件还是 50,互不影响。引用类型像“共享同一把钥匙”——a和b拿的是同一把钥匙,开的是同一个保险箱,谁进去改了里面的东西,另一个人再开箱看到的也就变了。
这两段代码在内存里其实是这样子的:
flowchart LR
subgraph 值类型
VA[a 的格子: 50] -. 拷贝.-> VB[b 的格子: 50]
VA2[a 改成 100] -. 不影响.-> VB
end
subgraph 引用类型
RA[a] --> BOX[(保险箱 name:wg)]
RB[b] --> BOX
RB2[改 b.name] -. 同一箱.-> BOX
end3. 深拷贝
function deepCopy(obj) {
// 步骤 1:如果传入的不是对象或者是 null,则直接返回(也是递归的终止条件)
if (typeof obj !== "object" || obj === null) {
return obj;
}
let result;
// 步骤 2:处理数组
if (Array.isArray(obj)) {
result = [];
for (let i = 0; i < obj.length; i++) {
result[i] = deepCopy(obj[i]);
}
} else {
// 步骤 3:处理普通对象
result = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepCopy(obj[key]);
}
}
}
// 步骤 4:返回全新的拷贝
return result;
}
// 示例使用
const original = {
a: 1,
b: [2, 3],
c: { d: 4 },
};
const copied = deepCopy(original);
console.log(copied);
copied.b.push(5);
console.log(original.b); // 输出: [2, 3]
console.log(copied.b); // 输出: [2, 3, 5]
二、原型和原型链
1. class 和继承
class 使用 constructor 构建,示例:
class Student {
constructor(name, age) {
this.name = name;
this.age = age;
}
SayHello() {
console.log(`你好,我叫 ${this.name},今年${this.age}岁了`);
}
}
let student = new Student("zhangsan", 18);
student.SayHello();
继承,使用 extends 关键字来完成继承,super 来执行父类的构造函数,示例:
class Student {
constructor(name, age) {
this.name = name;
this.age = age;
}
SayHello() {
console.log(`你好,我叫 ${this.name},今年${this.age}岁了`);
}
}
// 继承
class MiddleSchoolStudent extends Student {
constructor(name, age, task) {
super(name, age);
this.task = task;
}
CompleteTask() {
console.log(`${this.name}完成了${this.task}任务`);
}
}
let middleSchoolStudent = new MiddleSchoolStudent("zhangsan", 20, "英语听力");
middleSchoolStudent.SayHello(); // 你好,我叫 zhangsan,今年20岁了
middleSchoolStudent.CompleteTask(); // zhangsan完成了英语听力任务
2. 类型判断 instanceof
MiddleSchoolStudent instanceof Student // true
Student instanceof Object // true
[] instanceof Array // true
[] instanceof Object // true
{} instanceof Object // true
3. 原型和原型链
1). 原型
// class 实际上是函数,是个语法糖
typeof Student; // function
typeof MiddleSchoolStudent; // function
// 隐式原型__porto__和显式原型prototype
console.log(MiddleSchoolStudent.__proto__);
console.log(Student.prototype);
console.log(MiddleSchoolStudent.__proto__ === Student.prototype);

每个 class 都有显式原型
prototype每个实例都有隐式原型
__proto__实例的隐式原型
__proto__指向 class 的显式原型prototypeMiddleSchoolStudent 获取属性 name 或执行 SayHello()方法时,现在自身属性和方法寻找,如果找不到就到
__proto__中去查找

- MiddleSchoolStudent.hasOwnProperty(’name’) // true
- MiddleSchoolStudent.hasOwnProperty(‘SayHello’) // false

- Instancesof 比如 MiddleSchoolStudent instanceof Student,会沿着上述链条寻找
白话类比:原型链像“家族族谱 + 传家宝”。每个班级(class)有一本公共说明书(
prototype),每个学生(实例)手里有个指针(__proto__)指向这本说明书。你要用一个方法(比如SayHello),先翻自己的口袋,没有就顺着指针去翻班级的说明书,再没有就顺着说明书的指针去翻“老祖宗 Object 的说明书”。这条一路往上的查找链,就是原型链。
把这条查找链画成图:
flowchart TD
I[实例 middleSchoolStudent] -->|__proto__| P1[MiddleSchoolStudent.prototype]
P1 -->|__proto__| P2[Student.prototype]
P2 -->|__proto__| P3[Object.prototype]
P3 -->|__proto__| NULL[null 查找终止]三、作用域和闭包
1. this 的不同应用场景,如何取值
this 取什么值,是在函数执行时确定的,不是在函数定义的时候确定的
白话类比:
this像“这句话是谁说的”。同一个人(函数体)在不同场合(调用方式)下,“我”指代的人不一样:在街上自言自语(普通函数)时“我”指路人甲(window);被介绍给别人(call/bind)时“我”指那个对象;作为某人的秘书(对象方法)时“我”指那位老板;箭头函数则是“沿用上级介绍时的身份”。
1). 作为普通函数
function fn() {
console.log(this);
}
fn(); // this 是 window
2). 使用 call、apply、bind
// call 是函数对象的一个方法,其用途是改变函数内部 this 的指向,同时能在调用函数时传入参数
function fn() {
console.log(this);
}
fn.call({ x: 100 });
// this 是 {x:100}
// bind() 是函数对象的一个方法,它主要用于创建一个新的函数,在调用时这个新函数的 this 值会被绑定到指定的对象上,并且可以预设一些参数
const fn1 = fn.bind({ x: 200 });
fn1();
// this 是 {x:200}
3). 作为对象方法被调用
const zhangsan = {
name: "张三",
sayHello() {
console.log(this); // 这里的this指向当前对象
},
wait() {
setTimeout(function () {
console.log(this); // this === window,这个this是被setTimeout触发执行,所以指向setTimeout,而不是wait触发
});
},
};
4). 箭头函数
const zhangsan = {
name: "张三",
sayHello() {
console.log(this); // 这里的this指向当前对象
},
wait() {
setTimeout(() => {
console.log(this); // this即当前对象,箭头函数被setTimeout触发,但是箭头函数的this取得是箭头函数的上级作用域的this,就是wait()
});
},
};
5). 在 class 方法中被调用
class Student{
constructor(name,age) {
this.name = name;
this.age = age;
}
SayHello(){
console.log(this)
}
}
const zhangsan = New Student("张三",18)
zhangsan.SayHello() // this指向zhangsan对象
不同场景下 this 的取值,一图概括:
flowchart TD
A[普通函数 fn] -->|this = window| W[window]
B[call / apply / bind] -->|this = 指定对象| O[指定对象]
C[对象方法] -->|this = 当前对象| CUR[该对象]
D[箭头函数] -->|this = 上级作用域的 this| UP[定义处 this]
E[class 方法] -->|this = 调用实例| INS[zhangsan]2. 手写 bind 函数
Function.prototype.myBind = function (thisArg, ...args) {
// 步骤 1:保存原函数(this 指向被调用的函数)
const self = this;
// 步骤 2:返回一个新函数,调用时把 this 绑到 thisArg,并合并预设参数与新参数
return function (...newArgs) {
const allArgs = [...args, ...newArgs];
return self.apply(thisArg, allArgs);
};
};
// 测试代码
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person = { name: "John" };
const boundGreet = greet.myBind(person, "Hello");
boundGreet();
3. 实际开发中闭包的应用场景,举例说明
第一种情况:
函数作为返回值
function create() {
const a = 100;
return function () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 输出:100
console.log 中的 a 叫做自由变量,没有被赋值,它会在自己定义的位置向上级作用域查找,不是执行的地方,比如上述例子中,console.log
中的 a 定义在 create()函数中,它会向上查找,找到 const a=100 自己就赋值为 100
第二种情况:
函数作为参数
function print(fn) {
const a = 200;
fn();
}
const a = 100;
function fn() {
console.log(a);
}
print(fn); // 输出:100
console.log 中的 a 为自由变量,在自己定义的作用域中向上级作用域查找,不是执行的地方,最近的是 const a =100 所以输出 100
自测题与动手练习
自测题(合上书能答出来,才算懂):
- 值类型和引用类型在内存上有什么区别?为什么改了
b.name后a.name也跟着变? - 手写的
deepCopy为什么要先判断typeof obj !== "object" || obj === null?不判断会怎样? instanceof是靠什么判断“是否属于某个类”的?它和原型链有什么关系?- 实例访问一个自身没有的属性/方法时,查找顺序是怎样的?
__proto__和prototype分别属于谁? this在普通函数、对象方法、箭头函数、class 方法里分别取什么值?为什么箭头函数里的this和setTimeout回调里的普通函数不一样?
动手练习(建议真做一遍):
- 在控制台跑“值类型 vs 引用类型”两段代码,确认改
b不影响a、改b.name影响a.name,并用一句话解释原因。 - 给
deepCopy传一个含数组和嵌套对象的对象,改拷贝的数组后再打印原对象,验证两者互不影响。 - 写一个
myBind并测试:把普通函数的this绑到{ name: "John" },调用后确认this.name生效;再尝试预设参数,验证参数合并顺序。
本章小结
- 值类型是“复印”(独立格子),引用类型是“共享钥匙”(同一对象),赋值时引用类型只是多了一把指向同一对象的钥匙。
deepCopy的精髓是递归 + 终止条件:typeof obj !== "object" || obj === null必须最先判断,才能正确处理基本类型和null。class是函数语法糖,extends/super完成继承;instanceof本质是沿着原型链向上查找。- 原型链是属性/方法的查找链:实例
__proto__→ classprototype→ … →Object.prototype→null。 this在函数执行时确定:普通函数指向 window,call/bind 指定对象,对象方法指当前对象,箭头函数沿用上级作用域,class 方法指调用实例。- 下一篇我们接着看异步机制(Event Loop、Promise、async/await),把“同步执行”之外的世界讲清楚。