主题
值类型和引用类型(Day9 3/3)
本页对应 Day09_JavaScript,共 6 段视频,约 1 小时 27 分。
学习目标
- 能够区分
__proto__与prototype,并准确说明二者分别指向对象原型还是函数实例原型。 - 能够为数组实例、自定义构造函数实例等画出完整原型链,并据此判断属性访问结果。
- 能够使用
instanceof判断对象与构造函数的关系,并解释原型链上层构造函数也返回true的原因。 - 能够说明
constructor属性的查找过程,解释其为何可能不指向对象“自身构造函数”,并列举准确与不准确的场景。 - 能够根据给定对象、构造函数和原型关系,写出原型链节点并分析新增属性是否可被实例访问。
知识点
先说明材料边界
本节笔记标题是“4 值类型和引用类型”,但笔记正文只有四项待填模板;转写稿实际讲的是原型、原型链、构造函数、instanceof 和 constructor,结尾也明确说“值类型和引用类型不着急,下次上课再研究”。因此,下面不能编造“内存存储方式、赋值方式、可变不可变、判等方式”的具体结论;这四项在现有材料中没有展开。本节笔记也未附图片和可运行代码,转写稿提到的原型链图未随笔记提供,无法插入图片。下面把转写稿中可确认的原型链内容整理成教程正文,并保留转写中讨论的代码判断。
原型链为什么需要
转写稿一开始就在追问:数组实例、数组构造函数、Object、Function 之间到底是什么关系?为什么 arr instanceof Object 也成立?为什么 constructor 有时候“不准”?这些问题的共同点是:不能只看对象自己,而要看它沿原型链往上找。原型链解决的是“属性/方法找不到时往哪里继续找”以及“instanceof 如何判断”的问题。怎么想:先分清“实例”和“构造函数”是两条线;构造函数通过 prototype 找到实例原型,实例通过 __proto__ 找到自己的原型。怎么做:用控制台逐条比较。易错点:把 Array.__proto__ 和 Array.prototype 当成同一个东西;把“构造函数一样”误认为“对象相等”。
怎么想:构造函数与原型两条线
转写稿反复画图:数组实例 arr 的原型是 Array.prototype,再往上是 Object.prototype;而 Array 本身是函数,函数的原型是 Function.prototype,再往上是 Object.prototype。所以 Array.__proto__ 得到的是“Array 作为函数对象”的原型,不是“数组实例”的原型。转写中先出现判断写错、后来修正的过程,说明这类等式必须逐层写,不能凭“看起来像”判断。可整理为:
js
function F() {}
console.log(F.__proto__ === Function.prototype); // true
console.log(F.__proto__.__proto__ === Object.prototype); // true
console.log(Array.__proto__ === Function.prototype); // true
console.log(Array.__proto__ === Array.prototype); // false
console.log(Array.prototype.__proto__ === Object.prototype); // true易错点:Array.prototype 是数组实例的原型;Array.__proto__ 是 Array 这个函数对象的原型。两者不是一回事。
怎么做:用代码验证原型链
转写稿用数组举例,判断实例、原型和顶层原型的关系。整理为:
js
const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
console.log(arr instanceof Function); // false为什么 arr instanceof Object 也成立?因为 instanceof 不是只看“自己的构造函数”,而是沿原型链找:只要某个节点的构造函数匹配,就成立。易错点:arr instanceof Function 不成立,因为 arr 的原型链上没有以 Function 为构造函数的节点。转写稿还强调:普通对象 {} 和 new Object() 的原型链相同,不要被字面量写法干扰。
instanceof 的判断规则
转写稿的结论是:对象 instanceof 构造函数,首先看对象自己的构造函数是否匹配;其次看原型链上某个节点的构造函数是否匹配。比如自定义函数:
js
function Fn() {}
const user = new Fn();
console.log(user instanceof Fn); // true
console.log(user instanceof Object); // true
console.log(user instanceof Function); // false为什么 user instanceof Object 成立?因为 user 的原型链最终到 Object.prototype。为什么 user instanceof Function 不成立?因为 user 是普通实例,不是函数对象。易错点:把“构造函数的原型链”当成“实例的原型链”;转写稿说“各看各的”,这是关键。
constructor 的巨坑
转写稿专门提醒:constructor 不一定“准”。例如:
js
const arr = [];
console.log(arr.constructor === Array); // true
console.log(Array.prototype.constructor === Array); // true
console.log(Object.prototype.constructor === Object); // truearr.constructor 看起来是 Array,但 arr 自身不一定有 constructor,它可能是沿原型链从 Array.prototype 上拿到的。更关键的是:Array.prototype.constructor 的值是 Array,这个属性主要是“给以它为原型的实例准备”的,不是用来证明“它自己的构造函数就是 Array”。转写稿用红框强调:Array.prototype 自身的构造函数其实是 Object,但它上面的 constructor 显示为 Array,所以不能直接拿 constructor 判断“自身构造函数”。易错点:看到 constructor 就下结论;正确做法是先画原型链,再看这个属性是在哪个节点上、给谁用的。
特殊点:Function
转写稿最后指出两个“上帝”式特殊点:Object.prototype 是顶层原型;Function 能自己构造自己。可验证:
js
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true易错点:这是 Function 的特殊情况,不能套到普通函数或数组上。普通函数的 __proto__ 指向 Function.prototype,但 fn.prototype 是给实例用的原型对象,二者不同。转写稿反复说“构造函数一掺和就烦”,本质就是要把“实例原型线”和“函数对象原型线”分开画。
笔记中的四项待补项
原笔记模板仍保留为待补:内存存储方式、赋值方式(传值方式)、可变和不可变、判等方式。现有转写稿没有讲这四项,不能补写具体答案;需要等值类型/引用类型的正式讲解材料后再完成。
作业目标与先修概念
本节作业有两个任务:一是做原型链练习题,二是亲手画出包含 Array、function Product() {}、Object、Function 的原型链图。笔记里的原始任务先保留:
js
1. 原型链练习题
3. 画一画原型链的图
Array、function Product() {}、Object、Function
var arr = [];
var p = new Product();为什么需要画图?因为原型链本质是“属性找不到时,沿着 __proto__ 向上找”的规则。只背结论很容易把 prototype 和 __proto__ 混在一起。怎么想?每看到一个对象,先问两件事:它的构造函数是谁?它的原型是谁?怎么做?先画实例,再画构造函数,最后补 Object 和 Function。易错点是:arr.__proto__ 是 Array.prototype,不是 Object.prototype;Array.__proto__ 是 Function.prototype,不是 Array.prototype。
从 arr 和 p 出发找原型
先看数组实例。var arr = [] 的构造函数是 Array,所以 arr.__proto__ === Array.prototype 成立;而 Array.prototype 也是对象,它的 __proto__ 指向 Object.prototype。因此:
js
var arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__ === Object.prototype); // false
console.log(Array.prototype.__proto__ === Object.prototype); // true再看自定义构造函数:
js
function Product() {}
var p = new Product();
console.log(p.__proto__ === Product.prototype); // true
console.log(Product.prototype.__proto__ === Object.prototype); // true
console.log(p.constructor === Product); // true为什么需要这样验证?因为 new Product() 创建实例时,会把实例的 __proto__ 指向 Product.prototype。怎么想?Product.prototype 是“给实例用的原型对象”,p.__proto__ 才指向它。怎么做?用 === 逐条输出,不要只看“长得像”。易错点:Product.prototype 不是 Product.__proto__;Product.__proto__ 是 Function.prototype,因为 Product 本身是函数。
补上 Array、Object、Function 的构造函数链
函数类型特殊:函数既有 prototype,又有 __proto__。Array 是函数,所以 Array.__proto__ === Function.prototype;Object 也是函数,所以 Object.__proto__ === Function.prototype;自定义函数 Product 同样是函数,所以 Product.__proto__ === Function.prototype。可以这样验证:
js
console.log(Array.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Product.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true这里 Function.__proto__ === Function.prototype 是特殊自指:Function 的构造函数是它自己。易错点:不要因为 Array 能构造数组,就以为 Array.__proto__ 是 Array.prototype。Array.prototype 是数组实例的原型;Array.__proto__ 是 Array 这个函数对象的原型。
还要补顶层:Function.prototype.__proto__ === Object.prototype,而 Object.prototype.__proto__ === null。Object.prototype 是原型链终点,别再追问“它的构造函数是谁”来画链,否则容易乱。
画图顺序与检查清单
建议按这个顺序画:第一层画 arr、p;第二层画 Array.prototype、Product.prototype;第三层画 Object.prototype;再单独画构造函数 Array、Product、Object、Function。连线时用 __proto__ 表示“原型链查找方向”,用 prototype 表示“构造函数给实例准备的原型对象”,两者不要混线。
检查清单:arr.__proto__ → Array.prototype → Object.prototype → null;p.__proto__ → Product.prototype → Object.prototype → null;Array.__proto__ → Function.prototype → Object.prototype → null;Product.__proto__ → Function.prototype;Object.__proto__ → Function.prototype;Function.__proto__ → Function.prototype。易错点:constructor 只能辅助看“谁构造了谁”,不能替代原型链判断;系统内置对象的 constructor 还有“给实例使用”的设计,直接拿它判断原型链容易踩坑。
用练习验证
转写稿里的练习题可以这样验证:给 Array.prototype.name = '小乐',给 Function.prototype.age = 10,给 Object.prototype.address = '上海',则 arr.name 是 '小乐',arr.age 是 undefined,arr.address 是 '上海'。因为 arr 的原型链上没有 Function.prototype,所以找不到 age。而 Array.name 是 undefined,Array.age 是 10,Array.address 是 '上海',因为 Array.__proto__ 指向 Function.prototype。最后用 arr instanceof Array、arr instanceof Object 检查,二者都为 true;arr instanceof Function 为 false。这样图、代码、输出三者对得上,作业才算真正完成。
课堂画面






代码示例
代码示例一:值类型与引用类型
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>值类型与引用类型</title>
</head>
<body>
<pre id="out"></pre>
<script>
const out = document.getElementById('out');
const log = (label, value) => { out.textContent += label + ' → ' + value + '\n'; };
/* 1. 赋值方式:值类型传值,引用类型传地址 */
let a = 10;
let b = a; // 把 10 这个值本身复制一份给 b
b = 20;
log('a', a); // 10
log('b', b); // 20
const o1 = { n: 1 };
const o2 = o1; // 复制的是堆内存地址,o1 和 o2 指向同一个对象
o2.n = 2;
log('o1.n', o1.n); // 2
log('o2.n', o2.n); // 2
/* 2. 可变与不可变 */
let s = 'abc';
s.toUpperCase(); // 返回新字符串,原字符串不变
log('s', s); // abc
log('s.toUpperCase()', s.toUpperCase()); // ABC
/* 3. 判等方式:值类型比内容,引用类型比地址 */
log('10 === 10', 10 === 10); // true
log('{} === {}', {} === {}); // false,两个不同的堆内存
log('o1 === o2', o1 === o2); // true,同一个地址
</script>
</body>
</html>要点解释
let b = a之后改b不影响a,因为值类型复制的是值本身;const o2 = o1之后改o2.n会连带改o1.n,因为两者共享同一块堆内存。- 易错点:字符串是不可变的值类型,
s.toUpperCase()只是返回一个新串,s本身不会变。 - 易错点:
{} === {}永远是false,引用类型判等比的是内存地址,不是“长得像不像”。
代码示例二:原型链上的属性查找、instanceof、constructor
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原型链查找 / instanceof / constructor</title>
</head>
<body>
<pre id="out"></pre>
<script>
const out = document.getElementById('out');
const log = (label, value) => { out.textContent += label + ' → ' + value + '\n'; };
/* 在三条不同的原型链节点上挂属性(用自定义属性名,避开内置的 name 等) */
Array.prototype.myName = '小乐';
Function.prototype.myAge = 10;
Object.prototype.address = '上海';
const arr = [];
/* 数组实例的链:arr → Array.prototype → Object.prototype → null */
log('arr.myName', arr.myName); // 小乐
log('arr.myAge', arr.myAge); // undefined,Function.prototype 不在这条链上
log('arr.address', arr.address); // 上海
/* 构造函数自己的链:Array → Function.prototype → Object.prototype → null */
log('Array.myName', Array.myName); // undefined
log('Array.myAge', Array.myAge); // 10
log('Array.address', Array.address); // 上海
/* instanceof:看原型链上有没有这个构造函数的 prototype */
log('arr instanceof Array', arr instanceof Array); // true
log('arr instanceof Object', arr instanceof Object); // true
log('arr instanceof Function', arr instanceof Function); // false
function FN() {}
const user = new FN();
log('user instanceof FN', user instanceof FN); // true
log('user instanceof Object', user instanceof Object); // true
log('user instanceof Function', user instanceof Function); // false
/* constructor */
log('arr.constructor === Array', arr.constructor === Array); // true
log('Array.prototype.constructor === Array', Array.prototype.constructor === Array); // true
log('Object.prototype.constructor === Object', Object.prototype.constructor === Object); // true
</script>
</body>
</html>要点解释
- 属性查找只沿着
__proto__往上走,找到就停。arr.myAge是undefined,因为arr的链上根本没有Function.prototype——“构造函数的原型链”和“实例的原型链”是各看各的。 Array本身是函数,它的链是Array → Function.prototype → Object.prototype,所以Array.myAge(10)和Array.address(上海)都能找到,但Array.myName找不到。instanceof只要原型链上任意一个节点的构造函数匹配就返回true,所以arr instanceof Object也是true;但arr instanceof Function是false。- 易错点:
Array.prototype.constructor这个值并不是“给自己用的”,而是给数组实例准备的,所以用它去看“某个对象的构造函数”不一定准;自身本来就有constructor的对象(如Object.prototype)反而是准的。
代码示例三:原型链图的代码验证
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原型链图验证</title>
</head>
<body>
<pre id="out"></pre>
<script>
const out = document.getElementById('out');
const log = (label, value) => { out.textContent += label + ' → ' + value + '\n'; };
function Foo() {}
const f1 = new Foo();
const f2 = new Foo();
const o1 = new Object();
const o2 = {};
/* 第一条链:f1 / f2 共用同一条链 */
log('f1.__proto__ === Foo.prototype', f1.__proto__ === Foo.prototype); // true
log('f2.__proto__ === Foo.prototype', f2.__proto__ === Foo.prototype); // true
log('Foo.prototype.__proto__ === Object.prototype',
Foo.prototype.__proto__ === Object.prototype); // true
/* 第二条链:o1 / o2 一步到顶 */
log('o1.__proto__ === Object.prototype', o1.__proto__ === Object.prototype); // true
log('o2.__proto__ === Object.prototype', o2.__proto__ === Object.prototype); // true
/* 第三条链:Foo / Object / Function 三个函数本身 */
log('Foo.__proto__ === Function.prototype', Foo.__proto__ === Function.prototype); // true
log('Object.__proto__ === Function.prototype', Object.__proto__ === Function.prototype); // true
log('Function.__proto__ === Function.prototype',
Function.__proto__ === Function.prototype); // true,自己构造自己,唯一的特殊情况
/* 终点:Object.prototype.__proto__ 是 null,链到此为止 */
log('Function.prototype.__proto__ === Object.prototype',
Function.prototype.__proto__ === Object.prototype); // true
log('Object.prototype.__proto__', Object.prototype.__proto__); // null
</script>
</body>
</html>要点解释
- 这段代码对应作业要求画的那张图,一共只有三条原型链:
f1/f2一条、o1/o2一条、Foo/Object/Function三个函数本身一条。 Foo.prototype.__proto__ === Object.prototype说明函数原型对象本身也是Object的实例,链继续往上走,最终停在Object.prototype.__proto__ === null,它是“顶层原型”,不用再追问它的构造函数是谁。- 易错点:
Foo.__proto__(构造函数的原型,走Function.prototype)和Foo.prototype(它构造出来的实例的原型)是两个完全不同的对象,Array.__proto__与Array.prototype同理,不要混为一谈。 - 易错点:
Function.__proto__ === Function.prototype是整个函数体系里唯一的特例,因为Function的构造函数就是它自己;其他函数、数组都没有这种“自己等于自己原型”的关系。
常见错误
现象:把
arr.__proto__和Array.prototype混着比较,得出“构造函数原型就是实例原型”的结论。→ 原因:没有先分清“实例链”和“构造函数链”。→ 修法:先写清起点对象,再逐级标__proto__,不要跨链比较。现象:用
arr.constructor判断类型,遇到Array.prototype.constructor、Object.prototype.constructor时误判。→ 原因:constructor是可继承、可改的普通属性,原型上的值常常不是“给自己用”的。→ 修法:类型判断优先用instanceof、Array.isArray、Object.prototype.toString,或先画链再判断。现象:认为
arr instanceof Object应为false。→ 原因:只看了直接构造函数,忽略instanceof会沿原型链查找。→ 修法:判断前先列出完整原型链,看链上是否有某个构造函数的prototype。现象:修改“副本对象”后原对象也变了,或以为两个内容相同的对象用
===会相等。→ 原因:引用类型赋值复制的是地址,值类型复制的是内容。→ 修法:需要独立副本时用展开、深拷贝;判等时区分值比较与引用比较。现象:重写
F.prototype后,new F().constructor指向旧构造函数。→ 原因:constructor不会随prototype重写自动修正。→ 修法:重写原型时显式补constructor,或不要依赖constructor做类型判断。现象:把
Function.prototype、Object.prototype当普通对象凭直觉推,遇到自指或特殊节点就乱。→ 原因:特殊节点不符合“每个对象都按同一模板走”的直觉。→ 修法:先按规则画图,标清__proto__与prototype,特殊节点单独记忆,不强行套普通对象结论。
练习
写出下列结果并说明依据:
function F(){},const f = new F();
判断:f.__proto__ === F.prototype、F.__proto__ === Function.prototype、F.prototype.__proto__ === Object.prototype。按作业要求画图:包含
Array、function Product(){}、Object、Function、var arr = []、var p = new Product(),标出各对象的__proto__指向,并区分prototype与__proto__。判断并解释:
arr instanceof Array、arr instanceof Object、arr instanceof Function;p instanceof Product、p instanceof Object、p instanceof Function。执行后写出结果:
Array.prototype.x = 1;Function.prototype.y = 2;Object.prototype.z = 3;
判断:[].x、[].y、[].z、Array.x、Array.y、Array.z。写出最终值:
let a = { n: 1 }; let b = a; b.n = 2; let c = a.n; c = 3;
问:a.n是多少?再把a改成数字类型重做一次,比较两次差异。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 原型和构造函数01 | 16:50 |
| 2 | 原型和构造函数02 | 24:38 |
| 3 | 原型链题目分析 | 06:51 |
| 4 | instanceof运算符 | 06:58 |
| 5 | constructor | 10:19 |
| 6 | 原型链 | 22:03 |
小结
- 先分清“实例链”和“构造函数链”,再谈相等;不要拿不同链上的节点直接比较。
- 判断属性或
instanceof前,先定位起点对象,沿__proto__逐级看,不要跳到构造函数链。 constructor只当普通属性看,不能当可靠类型判断依据;类型判断优先用更稳定的方式。- 遇到
Function.prototype、Object.prototype等特殊节点,先接受规则并画图,不要用普通对象直觉硬推。 - 值类型与引用类型的核心在赋值和判等策略:是否需要独立副本决定拷贝方式。
- 画原型链图时先列对象/函数,再标
__proto__,最后标prototype;初学阶段不要一开始把constructor全画上。 - 做题顺序固定为“起点→逐级原型→是否命中→写答案”,比凭记忆猜更稳。