读完红宝书和YDKJS,我终于搞懂了原型链、闭包和this

这三个概念可以说是JS面试的"必考题",也是很多人从入门到放弃的拦路虎。今天用最通俗的方式聊聊我的理解。


前言

说实话,刚开始学JavaScript的时候,这三个概念真的让我头大。每次觉得自己懂了,过几天又忘了,面试的时候一紧张就乱说。

后来我把《JavaScript高级程序设计》(俗称红宝书)和《你不知道的JavaScript》(YDKJS)这两本书反复啃了几遍,终于算是把这块硬骨头啃下来了。今天把我的理解分享出来,希望能帮到同样困惑的小伙伴们。

这三个东西看似独立,其实都指向JavaScript的一个核心设计哲学------关系。原型链是对象之间的关系,闭包是函数和作用域的关系,this是函数和调用者的关系。理解了这一点,很多东西就通了。


一、原型链:找不到就往上问

1.1 先抛个结论

原型链就是JavaScript的"继承"机制------对象自己没有某个属性时,会沿着一条链往上找,找到就返回,找不到就继续找,直到null为止。

就这么简单。但很多人(包括以前的我)容易把它想复杂了。

1.2 三个角色要分清

先看个关系:

javascript 复制代码
构造函数 Person
    │
    │ new
    ▼
实例对象 ──→ Person.prototype ──→ Object.prototype ──→ null

这里面有三个角色:

  • 构造函数 :就是那个function Person() {},用来生产对象的"工厂"
  • 原型对象Person.prototype,所有实例共享的属性和方法都放这儿
  • 实例对象new Person()出来的那个东西

它们之间的关系是这样的:

javascript 复制代码
let p = new Person();

p.__proto__ === Person.prototype          // true,实例的__proto__指向原型
Person.prototype.constructor === Person   // true,原型的constructor指向构造函数
Person.prototype.__proto__ === Object.prototype  // true,原型也是对象,所以它也有原型
Object.prototype.__proto__ === null       // true,到头了

我当时理解这个关系的时候,画了好多遍图,最后发现记住一句话就行:实例通过__proto__找到原型,原型通过constructor找到构造函数,原型自己也是对象,所以它还有原型,一路往上直到null

1.3 属性查找过程

当你访问p.name的时候,JS引擎会这么干:

  1. 先在p自己身上找 → 有就返回
  2. 没找到?去p.__proto__(也就是Person.prototype)上找
  3. 还没有?去Person.prototype.__proto__(也就是Object.prototype)上找
  4. 还没有?到null了,返回undefined

这个过程就是原型链查找 。红宝书里讲得很详细,YDKJS则强调了一个点:原型链的本质是"委托",不是"复制"。对象A没有某个属性,它不是把属性复制过来,而是"委托"给原型去查找。

1.4 一个经典的坑

这个坑我踩过好几次,分享给大家:

javascript 复制代码
function Person() {}
Person.prototype.friends = ['Alice', 'Bob'];

let a = new Person();
let b = new Person();

a.friends.push('Charlie');
console.log(b.friends); // ['Alice', 'Bob', 'Charlie'] 😱

卧槽,b的friends怎么也被改了?

原因很简单:引用类型放在原型上,所有实例共享同一个引用。你改的不是a的friends,而是原型上的friends,所以b也受影响。

解决方案:引用类型属性在构造函数里初始化,别放原型上

javascript 复制代码
function Person() {
    this.friends = ['Alice', 'Bob'];  // 每个实例都有自己的friends
}

1.5 红宝书 vs YDKJS

这两本书对原型链的侧重点不太一样:

视角 红宝书 YDKJS
重点 继承模式(原型链继承、组合继承等) 原型链的本质是委托
核心观点 通过prototype实现对象间的关系 [[Get]]操作沿原型链查找,是行为委托机制
关键提醒 原型上的引用类型会被所有实例共享 Object.create()创建干净的原型关联

我个人觉得YDKJS的视角更"本质",红宝书更"实用"。两本结合起来看效果最好。


二、闭包:函数带着出生证明出门

2.1 一句话定义

闭包 = 函数 + 它能记住的外部变量。

函数被定义的时候,会"打包"周围的作用域。即使外部函数已经执行完毕,内部函数仍然能访问那些变量。

2.2 词法作用域是前提

要理解闭包,先得理解词法作用域

简单说就是:函数的作用域在写代码的时候就确定了,不是在调用的时候确定的。

javascript 复制代码
function outer() {
    let name = 'JavaScript';
    function inner() {
        console.log(name);  // inner在定义时就知道自己能访问name
    }
    return inner;
}

let fn = outer();
fn();  // 'JavaScript' ------ outer已经执行完了,但name还在!

这就是闭包。inner函数"记住"了它出生时的环境。

我当时学这块的时候,最大的困惑是:outer都执行完了,name变量不应该被回收吗?

答案是:不会,因为inner还引用着name,垃圾回收器不会回收被引用的变量。

这就是闭包存在的根本原因。

2.3 闭包的实际用途

闭包不是什么高深的东西,我们每天都在用。

用途一:数据私有化

javascript 复制代码
function createCounter() {
    let count = 0;  // 外部拿不到这个count

    return {
        increment() { return ++count; },
        decrement() { return --count; },
        getCount()   { return count; }
    };
}

let counter = createCounter();
counter.increment();  // 1
counter.increment();  // 2
counter.count;        // undefined,拿不到!

这个模式在模块化编程里特别常见,把数据"藏"起来,只暴露操作方法。

用途二:函数工厂(柯里化)

javascript 复制代码
function multiply(x) {
    return function(y) {
        return x * y;
    };
}

let double = multiply(2);
let triple = multiply(3);

double(5);   // 10
triple(5);   // 15

每次调用multiply都会创建一个新的闭包,各自记住自己的x

用途三:事件处理

javascript 复制代码
function setupButton(label) {
    let clicked = false;
    document.getElementById('btn').addEventListener('click', function() {
        if (!clicked) {
            console.log(label + '被点击了');
            clicked = true;
        }
    });
}

事件处理函数通过闭包记住了labelclicked

2.4 经典的循环陷阱

这个题我面试被问过好几次:

javascript 复制代码
// ❌ 错误写法
for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
// 输出:4, 4, 4(不是1, 2, 3!)

为啥是三个4?因为var没有块级作用域,三个回调函数共享同一个i,循环结束时i = 4

解决方案:

javascript 复制代码
// ✅ 方案1:用let(推荐)
for (let i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
// 输出:1, 2, 3

// ✅ 方案2:IIFE创建新作用域
for (var i = 1; i <= 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 1000);
    })(i);
}

ES6之后用let就完事了,简单粗暴。

2.5 红宝书 vs YDKJS

视角 红宝书 YDKJS
定义 有权访问另一个函数作用域中变量的函数 函数在定义时的词法环境组合
重点 闭包的用法(模块模式、私有变量) 闭包的本质是作用域气泡不会被GC回收
关键提醒 闭包可能导致内存泄漏,要及时解除引用 循环中的闭包陷阱源于对作用域理解不足

YDKJS对闭包本质的挖掘更深,红宝书更注重实践。


三、this指向:谁叫我,我就是谁

3.1 核心原则

this不是写代码的时候确定的,是调用的时候确定的。谁调用这个函数,this就指向谁。

这句话我背了好久,但真正理解是在写了大量代码之后。

3.2 四条绑定规则

YDKJS把this的绑定规则总结为四条,按优先级从低到高:

规则一:默认绑定(最低优先级)

javascript 复制代码
function sayThis() {
    console.log(this);
}

sayThis();  // 浏览器中是window,严格模式下是undefined

没有任何修饰的函数调用,this指向全局对象(非严格模式)或undefined(严格模式)。

规则二:隐式绑定

javascript 复制代码
let obj = {
    name: 'JS',
    say() {
        console.log(this.name);
    }
};

obj.say();  // 'JS' ------ this指向obj

通过对象调用函数(obj.fn()),this指向那个对象。

⚠️ 但这里有个大坑:隐式丢失

javascript 复制代码
let fn = obj.say;
fn();  // undefined ------ 函数被"单独拿出来了",变成默认绑定!

我以前经常在这里翻车。把方法赋值给变量后,它就不再是"obj的方法"了,就是一个普通函数,this丢失了。

规则三:显式绑定(call/apply/bind)

javascript 复制代码
function greet() {
    console.log(this.name);
}

let person = { name: 'Alice' };

greet.call(person);    // 'Alice'
greet.apply(person);   // 'Alice'

let bound = greet.bind(person);
bound();               // 'Alice'

手动指定this的指向。callapply立即执行,bind返回新函数。

三者的区别:

方法 参数形式 是否立即执行
call fn.call(thisArg, arg1, arg2) ✅ 立即执行
apply fn.apply(thisArg, [arg1, arg2]) ✅ 立即执行
bind fn.bind(thisArg, arg1) ❌ 返回新函数

规则四:new绑定(最高优先级)

javascript 复制代码
function Person(name) {
    this.name = name;
}

let p = new Person('Bob');
console.log(p.name);  // 'Bob'

使用new调用函数时,this指向新创建的对象。

new到底做了什么?

  1. 创建一个空对象{}
  2. 把这个空对象的[[Prototype]]指向构造函数的prototype
  3. this绑定到这个空对象
  4. 执行构造函数里的代码
  5. 如果构造函数返回对象就用那个对象,否则返回新创建的对象

3.3 优先级总结

arduino 复制代码
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

记住这个顺序,面试的时候能快速判断this指向。

3.4 箭头函数是个特例

箭头函数没有自己的this,它的this继承自定义时 外层作用域的this

javascript 复制代码
let obj = {
    name: 'JS',
    say: () => {
        console.log(this.name);  // undefined
    },
    sayLater() {
        setTimeout(() => {
            console.log(this.name);  // 'JS'
        }, 100);
    }
};

obj.say();      // undefined ------ 箭头函数的this是外层的this(window)
obj.sayLater(); // 'JS' ------ 箭头函数继承了sayLater的this

关键区别

  • 普通函数:this取决于怎么调用
  • 箭头函数:this取决于在哪定义

所以箭头函数特别适合用在回调里,不用再担心this丢失的问题。

3.5 this陷阱速查表

场景 this指向
obj.fn() obj
fn() 全局 / undefined(严格模式)
fn.call(obj) obj
fn.bind(obj)() obj
new Fn() 新创建的对象
setTimeout(fn) 全局 / undefined(严格模式)
() => {} 定义时外层的this
事件回调 绑定事件的DOM元素

四、三者的内在联系

写到这儿,不知道大家有没有发现,这三个概念其实都指向JavaScript的一个核心设计------关系的建立与查找

概念 核心问题 查找方向 确定时机
原型链 属性在哪儿? 当前对象 → 原型 → ... 定义时
闭包 变量能访问吗? 当前作用域 → 外层作用域 → ... 定义时
this 函数为谁服务? 调用方式决定 调用时

原型链和闭包都是定义时确定 的,this是调用时确定的。理解了这一点,很多问题就豁然开朗了。


五、来道面试题练练手

这是一道经典的综合题,把原型链、变量提升、this都考到了:

javascript 复制代码
function Foo() {
    getName = function() { console.log(1); };
    return this;
}
Foo.getName = function() { console.log(2); };
Foo.prototype.getName = function() { console.log(3); };
var getName = function() { console.log(4); };
function getName() { console.log(5); }

Foo.getName();           // ?
getName();               // ?
Foo().getName();         // ?
getName();               // ?
new Foo.getName();       // ?
new Foo().getName();     // ?

答案和解析:

javascript 复制代码
Foo.getName();           // 2 ------ 静态属性,直接访问
getName();               // 4 ------ 变量提升后,函数声明被var赋值覆盖
Foo().getName();         // 1 ------ Foo()没有new,this指向window,修改了全局getName
getName();               // 1 ------ 被上一步改了
new Foo.getName();       // 2 ------ 先取Foo.getName,再new这个函数
new Foo().getName();     // 3 ------ new Foo()返回实例,沿原型链找到3

这道题的几个关键点:

  1. 区分静态属性和原型属性Foo.getName vs Foo.prototype.getName
  2. 变量提升function getName会被var getName = ...覆盖
  3. this指向Foo()没有newthis指向window
  4. 运算符优先级new Foo.getName()new (Foo.getName)()

写在最后

这三个概念可以说是JavaScript的基石,理解了它们,面向对象和函数式编程的大门就打开了。

最后送大家三句话:

  • 原型链:找不到属性就往上问,一直问到null
  • 闭包:函数带着出生证明出门,到哪都记得自己从哪来
  • this:谁叫我,我就是谁(箭头函数除外,它出生就认了干爹)

希望这篇分享对你有帮助!有问题欢迎评论区交流


相关书籍推荐

  • 《JavaScript高级程序设计》(红宝书)------ 全面系统
  • 《你不知道的JavaScript》(上中卷)------ 深入本质
相关推荐
用户11489669441051 小时前
JavaScript原型链解析
javascript
Cosolar1 小时前
大模型量化技术全景深度解析:从FP16到INT4的完整演进与实战落地
人工智能·面试·架构
Mahir1 小时前
面试被问 MySQL 慢 SQL 怎么排查?看完这篇直接给面试官讲明白
面试
IT当时语_青山师__JAVA技术栈1 小时前
动态代理深度解析:JDK与CGLIB底层实现与实战
java·后端·面试
白露与泡影2 小时前
2026年Java面试最全避坑指南:从基础、并发、JVM到微服务,这一篇就够了
java·jvm·面试
Mr数据杨2 小时前
【Codex】用APP绑定教程模块规范移动端接入指引
java·前端·javascript·django·codex·项目开发
叼烟扛炮2 小时前
C++第五讲:内存管理
c++·算法·面试·内存管理
博客zhu虎康2 小时前
小程序按钮实现先表单校验再走手机号获取功能
android·javascript·小程序
超级无敌谢大脚2 小时前
【无标题】
开发语言·前端·javascript