Skip to content

值类型和引用类型(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); // true

arr.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。这样图、代码、输出三者对得上,作业才算真正完成。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码示例一:值类型与引用类型 ​

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 的构造函数就是它自己;其他函数、数组都没有这种“自己等于自己原型”的关系。

常见错误 ​

  1. 现象:把 arr.__proto__ 和 Array.prototype 混着比较,得出“构造函数原型就是实例原型”的结论。→ 原因:没有先分清“实例链”和“构造函数链”。→ 修法:先写清起点对象,再逐级标 __proto__,不要跨链比较。

  2. 现象:用 arr.constructor 判断类型,遇到 Array.prototype.constructor、Object.prototype.constructor 时误判。→ 原因:constructor 是可继承、可改的普通属性,原型上的值常常不是“给自己用”的。→ 修法:类型判断优先用 instanceof、Array.isArray、Object.prototype.toString,或先画链再判断。

  3. 现象:认为 arr instanceof Object 应为 false。→ 原因:只看了直接构造函数,忽略 instanceof 会沿原型链查找。→ 修法:判断前先列出完整原型链,看链上是否有某个构造函数的 prototype。

  4. 现象:修改“副本对象”后原对象也变了,或以为两个内容相同的对象用 === 会相等。→ 原因:引用类型赋值复制的是地址,值类型复制的是内容。→ 修法:需要独立副本时用展开、深拷贝;判等时区分值比较与引用比较。

  5. 现象:重写 F.prototype 后,new F().constructor 指向旧构造函数。→ 原因:constructor 不会随 prototype 重写自动修正。→ 修法:重写原型时显式补 constructor,或不要依赖 constructor 做类型判断。

  6. 现象:把 Function.prototype、Object.prototype 当普通对象凭直觉推,遇到自指或特殊节点就乱。→ 原因:特殊节点不符合“每个对象都按同一模板走”的直觉。→ 修法:先按规则画图,标清 __proto__ 与 prototype,特殊节点单独记忆,不强行套普通对象结论。

练习 ​

  1. 写出下列结果并说明依据:
    function F(){},const f = new F();
    判断:f.__proto__ === F.prototype、F.__proto__ === Function.prototype、F.prototype.__proto__ === Object.prototype。

  2. 按作业要求画图:包含 Array、function Product(){}、Object、Function、var arr = []、var p = new Product(),标出各对象的 __proto__ 指向,并区分 prototype 与 __proto__。

  3. 判断并解释:
    arr instanceof Array、arr instanceof Object、arr instanceof Function;
    p instanceof Product、p instanceof Object、p instanceof Function。

  4. 执行后写出结果:
    Array.prototype.x = 1;
    Function.prototype.y = 2;
    Object.prototype.z = 3;
    判断:[].x、[].y、[].z、Array.x、Array.y、Array.z。

  5. 写出最终值:
    let a = { n: 1 }; let b = a; b.n = 2; let c = a.n; c = 3;
    问:a.n 是多少?再把 a 改成数字类型重做一次,比较两次差异。

原视频对照 ​

#视频时长
1原型和构造函数0116:50
2原型和构造函数0224:38
3原型链题目分析06:51
4instanceof运算符06:58
5constructor10:19
6原型链22:03

小结 ​

  1. 先分清“实例链”和“构造函数链”,再谈相等;不要拿不同链上的节点直接比较。
  2. 判断属性或 instanceof 前,先定位起点对象,沿 __proto__ 逐级看,不要跳到构造函数链。
  3. constructor 只当普通属性看,不能当可靠类型判断依据;类型判断优先用更稳定的方式。
  4. 遇到 Function.prototype、Object.prototype 等特殊节点,先接受规则并画图,不要用普通对象直觉硬推。
  5. 值类型与引用类型的核心在赋值和判等策略:是否需要独立副本决定拷贝方式。
  6. 画原型链图时先列对象/函数,再标 __proto__,最后标 prototype;初学阶段不要一开始把 constructor 全画上。
  7. 做题顺序固定为“起点→逐级原型→是否命中→写答案”,比凭记忆猜更稳。