Skip to content

原型与原型链(Day9 2/3) ​

本页对应 Day09_JavaScript,共 6 段视频,约 1 小时 6 分。

学习目标 ​

  1. 能解释将实例共享的方法放在原型上的原因,并说明其节省内存空间的机制。
  2. 能使用自定义构造函数创建实例,并将需要共享的方法正确添加到构造函数的 prototype 上。
  3. 能判断给定属性位于实例自身还是原型上,并正确区分 in 与 hasOwnProperty 的判断结果。
  4. 能分析对象属性读取与赋值时的查找规则,说明给实例属性赋值为何不会直接修改原型上的同名属性。
  5. 能使用 Object.create 在创建对象时指定原型,或创建没有原型的对象,并验证其原型关系。
  6. 能描述原型链的属性查找过程,判断对象原型链的终点,并能结合值类型与引用类型说明赋值和比较时的行为差异。

知识点 ​

1. 为什么方法要放在原型上 ​

为什么需要: 数组实例会有很多个,若每个实例都各自保存一份 push、pop、forEach 等方法,就会重复占用内存。方法的功能通常相同,适合共享。

怎么想: 先观察一个数组:元素和 length 在数组对象自身上,而 push、pop 等方法在它的原型上。原型对象可以被同一构造函数创建的多个实例共同访问。

怎么做: 可以这样验证:

js
const arr1 = [1, 2, 3, 4, 5, 6, 7];
const arr2 = ['hello', '小乐'];

console.log(arr1.length); // 7
console.log(arr2.length); // 2
console.log(arr1.push === arr2.push); // true,方法来自同一原型

易错点: 不要把所有属性都搬到原型上。像 length 这种“不同实例值往往不同”的属性应放在实例自身;像方法这种“不同实例功能一样”的成员才适合放原型。转写中反复提到的 Lice、line词,实际都是 length。

2. 自定义构造函数:实例属性放自身,方法放原型 ​

为什么需要: 自定义构造函数可能创建成千上万个实例。若每个实例都保存相同方法,仍然会重复存储。

怎么想: 构造函数里的 this.name = name 会把属性加到新对象自身上;而方法可以挂到 User.prototype 上,让所有实例共享。

怎么做:

js
function User(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
}

User.prototype.addShopCart = function (product) {
  console.log(this.name + '将' + product + '添加到购物车');
};

User.prototype.buy = function (product) {
  console.log(this.name + '购买了' + product);
};

const u1 = new User('高尔乐', 89, '上海');
const u2 = new User('贾宝玉', 17, '南京');

u1.addShopCart('洗衣粉');
u1.buy('肥皂');
u2.addShopCart('方便面');
u2.buy('火腿肠');

输出中,u1、u2 自身只有 name、age、address,方法在原型上。this.name 会随调用者变化。

易错点: 构造函数中写 this.xxx = ... 只会加到实例自身,不会自动加到原型。方法要显式写到 构造函数.prototype 上。转写中“圆型”“言行”“port头”应统一纠正为“原型”“原型”“prototype”。

3. 读取与赋值不同:别误改原型 ​

为什么需要: 原型上的属性可以被实例“读到”,但给实例赋值时行为不同,容易误以为改了原型。

怎么想: 读取属性时,先找对象自身,找不到再沿原型链找;给属性赋值时,若自身没有该属性,会直接在自身上新增,而不是修改原型。

怎么做:

js
const arr01 = [];
arr01.name = '高乐';
arr01.__proto__.age = 100;
arr01.__proto__.address = '上海';

const arr02 = [];
console.log(arr02.name);    // undefined
console.log(arr02.age);     // 100
console.log(arr02.address); // 上海

arr02.address = '北京';
console.log(arr01.address); // 上海
console.log(arr02.address); // 北京

调试时展开 arr02 可看到:赋值后 address 被加到了 arr02 自身。

易错点: arr02.address = '北京' 不会改原型,也不会影响 arr01。要改原型,应通过 arr02.__proto__ 或 构造函数.prototype 获取原型后再改。

4. in 与 hasOwnProperty 的区别 ​

为什么需要: 判断“能不能用某属性”和“属性是否在对象自身上”是两件事。

怎么想: in 会查自身和原型链;hasOwnProperty 只查自身。

怎么做:

js
const arr = [];
arr.name = '高乐';
arr.__proto__.address = '上海';

console.log('length' in arr); // true
console.log('push' in arr);   // true
console.log('name' in arr);   // true
console.log('address' in arr);// true
console.log('h' in arr);      // false

console.log(arr.hasOwnProperty('name'));    // true
console.log(arr.hasOwnProperty('address')); // false
console.log(arr.hasOwnProperty('length'));  // true
console.log(arr.hasOwnProperty('push'));    // false

易错点: push 能用,但不在数组自身,所以 in 为 true,hasOwnProperty 为 false。转写中“海斯欧呢property”即 hasOwnProperty。

5. 创建对象时指定原型:Object.create ​

为什么需要: 有时希望创建对象时就指定它的原型,而不是先创建再修改。

怎么想: Object.create(原型对象) 返回新对象,并把参数作为新对象的原型;参数必须是对象或 null。

怎么做:

js
const obj1 = {};
console.log(Object.getPrototypeOf(obj1) === Object.prototype); // true

const protoArr = [];
const obj2 = Object.create(protoArr);
console.log(Object.getPrototypeOf(obj2) === protoArr); // true

const obj3 = Object.create(new String('hello'));
console.log(Object.getPrototypeOf(obj3)); // 字符串包装对象

const obj4 = Object.create(null);
console.log(Object.getPrototypeOf(obj4)); // null

转写中演示 Object.create('hello') 会报错,因为字符串原始值不是对象;写成 new String('hello') 才可以。调试时还出现过 Object.create 拼写错误,需注意大小写。

易错点: Object.create(null) 创建的是没有原型的对象,不是 {}。普通 {} 的原型默认是 Object.prototype。

6. 原型链与属性查找 ​

为什么需要: 原型本身也是对象,也有原型,于是形成链。理解它才能解释属性从哪里来。

怎么想: 查找属性时:先找对象自身;没有就沿 __proto__ 向上找;找到就停止;到顶层 Object.prototype 还没有,就返回 undefined。

怎么做:

js
const arr = [];
console.log(arr.__proto__ === Array.prototype);       // true
console.log(Array.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__);              // null

function Func() {}
const f = new Func();
console.log(f.__proto__ === Func.prototype);          // true
console.log(Func.prototype.__proto__ === Object.prototype); // true

数组的原型链大致是:arr → Array.prototype → Object.prototype → null。函数的原型链大致是:Func.prototype → Object.prototype → null。普通对象 {} 则直接到 Object.prototype。

易错点: 调试工具里 Function.prototype 可能显示得像函数,但它不是函数对象,而是 Object 类型的对象;不要被显示样式误导。原型链通常很短,不要被“链”字吓到。

一、为什么方法要放到原型上 ​

为什么需要:如果每创建一个数组,都把 push、pop、forEach 等方法复制一份到数组自身,创建很多数组时会重复占用空间。
怎么想:同一个构造函数创建出的不同实例,属性值往往不同,比如数组元素和 length;但方法的功能通常相同,适合放到原型上共享。
怎么做:观察两个数组,自身只保存元素和 length,方法从 Array.prototype 上查找。

js
const arr1 = [1, 2, 3, 4, 5, 6, 7];
const arr2 = ['hello', '小乐'];

console.log(arr1.length); // 7
console.log(arr2.length); // 2
console.log(arr1.push);   // 来自 Array.prototype

易错点:length 是数组实例自身属性,不是原型方法;push 虽然不在实例自身,但通过原型链仍可调用。

二、自定义构造函数时把方法放到 prototype ​

为什么需要:自定义 User 构造函数可能创建成千上万个用户对象,若每个对象都保存一份相同方法,会浪费内存。
怎么想:name、age、address 每个实例往往不同,放在 this 上;addShopCart、buy 方法逻辑相同,放到 User.prototype 上。
怎么做:

js
function User(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
}

User.prototype.addShopCart = function (product) {
  console.log(this.name + '将' + product + '添加到购物车');
};

User.prototype.buy = function (product) {
  console.log(this.name + '购买了' + product);
};

const u1 = new User('高尔乐', 89, '上海');
const u2 = new User('贾宝玉', 17, '南京');

u1.addShopCart('洗衣粉');
u1.buy('肥皂');
u2.addShopCart('方便面');
u2.buy('火腿肠');

易错点:若写成 this.addShopCart = function () {},每个实例都会保存一份函数;原型方法中的 this 由调用者决定,u1 调用时指向 u1,u2 调用时指向 u2。

三、读取和赋值经过原型链时不一样 ​

为什么需要:解释“为什么 arr02 能读到原型上的 address,但给 arr02.address 赋值后 arr01.address 不变”。
怎么想:读取属性时先找自身,找不到沿原型链找;赋值时若自身没有该属性,就在自身上新增,不会修改原型。
怎么做:

js
const arr01 = [];
arr01.name = '高尔乐';
arr01.__proto__.age = 100;
arr01.__proto__.address = '上海';

console.log(arr01.name);    // 高尔乐
console.log(arr01.age);     // 100
console.log(arr01.address); // 上海

const arr02 = ['A', 'B', 'C'];
console.log(arr02.name);    // undefined
console.log(arr02.age);     // 100
console.log(arr02.address); // 上海

arr02.address = '北京';
console.log(arr01.address); // 上海
console.log(arr02.address); // 北京

易错点:arr02.address = '北京' 只是在 arr02 自身上新增属性,并没有改原型;要改原型需通过 arr02.__proto__.address 或 Array.prototype.address。

四、in 与 hasOwnProperty 的区别 ​

为什么需要:判断一个属性“能不能用”和“是不是对象自己的”,是两个不同问题。
怎么想:in 会查整条原型链;hasOwnProperty 只查对象自身。
怎么做:

js
const arr = [10, 20, 30];
arr.userName = '高尔乐';
arr.__proto__.address = '上海';

console.log('length' in arr);        // true
console.log('push' in arr);          // true
console.log('address' in arr);       // true
console.log('age' in arr);           // false

console.log(arr.hasOwnProperty('userName')); // true
console.log(arr.hasOwnProperty('address'));  // false
console.log(arr.hasOwnProperty('push'));     // false

易错点:原型上的属性用 in 判断为 true,用 hasOwnProperty 判断为 false;只有自身和原型都没有时,in 才为 false。

五、创建对象时指定原型:Object.create ​

为什么需要:有时希望在创建对象的同时直接指定它的原型,而不是先创建再修改。
怎么想:Object.create(proto) 返回一个新对象,新对象的原型就是传入的参数。
怎么做:

js
const obj2 = Object.create([1, 2, 3]);
console.log(obj2.__proto__); // 数组对象

const obj3 = Object.create(new String('hello'));
console.log(obj3.__proto__); // 字符串包装对象

const obj4 = Object.create(null);
console.log(obj4.__proto__); // undefined,没有原型

易错点:参数必须是对象或 null;原始字符串 'hello' 不能直接作为原型,需要 new String('hello') 包装;Object.create(null) 创建的对象没有 toString 等原型方法。

六、原型链与属性查找终点 ​

为什么需要:原型本身也是对象,原型还有原型,需要说明属性查找如何结束。
怎么想:数组实例 → Array.prototype → Object.prototype → null;函数实例 → Function.prototype → Object.prototype → null。
怎么做:

js
const arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype);              // true
console.log(arr.__proto__.__proto__ === Object.prototype);   // true
console.log(Object.prototype.__proto__);                     // null

function func() {}
console.log(func.__proto__ === Function.prototype);          // true
console.log(func.__proto__.__proto__ === Object.prototype);  // true

易错点:Array.prototype 不是数组;函数的原型对象也不是函数,而是 Object 类型的对象;查找属性到 Object.prototype 仍找不到时返回 undefined,不会报错。

先明确:值类型与引用类型到底在解决什么问题 ​

我们写程序时,变量里到底存的是“数据本身”,还是“找到数据的地址”?如果不区分这件事,后面就会出现“我明明只改了 B,为什么 A 也变了”“两个对象看起来一样,为什么不相等”等问题。所以这一节先建立分类意识:值类型关注“值本身”,引用类型关注“对象引用”。

本节笔记先给出对比框架,下面把它填完整:

js
1. 内存存储方式
   值类型:变量中直接保存值本身
   引用类型:变量中保存对象的引用(地址),对象内容在堆中

2. 赋值方式(传值方式)
   值类型:复制值,两个变量互不影响
   引用类型:复制引用,两个变量可能指向同一个对象

3. 可变和不可变
   值类型:值本身不可变,改变量只是重新赋值
   引用类型:对象内容通常可变,可增删改属性

4. 判等方式
   值类型:通常比较值是否相同
   引用类型:通常比较是否指向同一个对象

值类型:为什么需要“复制值” ​

值类型常见有 number、string、boolean、null、undefined、symbol、bigint。需要它,是因为很多场景下我们希望变量之间彼此独立。怎么想?把变量想成一个小盒子,盒子里直接放数字或字符串。怎么做?赋值时复制一份:

js
let a = 10;
let b = a;
b = 20;
console.log(a); // 10

这里改 b 不会影响 a。易错点是:字符串方法看起来“改字符串”,其实是返回新字符串,例如 s.toUpperCase() 不会原地改 s;另外 const 只限制变量重新赋值,不改变值类型本身“不可变”的语义。

引用类型:为什么数组、对象、函数要保存引用 ​

引用类型包括对象、数组、函数等。需要引用,是因为对象可能很大,复制整个对象成本高;而且很多场景下我们确实希望多个变量操作同一个对象。怎么想?变量里放的是“门牌号”,真正的房子是堆里的对象。怎么做?看赋值:

js
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4]

arr2 = arr1 复制的是引用,两个变量指向同一个数组,所以通过 arr2 修改内容,arr1 也能看到。转写稿里用数组画图说明:数组对象本身放元素和 length,而 push、pop 等方法在原型上。这样设计是为了节省存储空间:创建很多数组时,不必每个数组都重复存一份方法,大家共享原型上的方法。易错点:const arr = [] 仍可 arr.push(1),因为不能改的是变量绑定,不是对象内容。

赋值与属性查找:读和写不是一回事 ​

为什么需要理解属性查找?因为引用类型常通过原型共享属性,读属性和写属性行为不同。怎么想?读取时先找对象自身,找不到再沿原型链向上找;赋值时若自身没有该属性,通常会在自身新增属性,而不是改原型。转写稿中的调试过程很典型:

js
let ar01 = [1, 2, 3];
ar01.name = "高考";
ar01.__proto__.age = 100;
ar01.__proto__.address = "上海";

let ar02 = ["A", "B", "C"];
console.log(ar02.name);    // undefined
console.log(ar02.age);     // 100
console.log(ar02.address); // 上海

因为 age、address 加在原型上,ar02 自身没有,但读取时会沿原型找到。接着:

js
ar02.address = "北京";
console.log(ar01.address); // 上海
console.log(ar02.address); // 北京

易错点:ar02.address = "北京" 不是改原型,而是给 ar02 自身新增 address;ar01 自身没有 address,仍读原型上的“上海”。要改原型,需要拿到原型对象再改。这个例子说明引用类型“共享”的是原型上的属性,但赋值可能产生自身属性。

判等:为什么两个“看起来一样”的对象不相等 ​

值类型判等通常看值,引用类型判等看引用。为什么?因为对象变量保存的是地址。怎么做?

js
console.log(1 === 1);                 // true
console.log("a" === "a");             // true
console.log([1, 2] === [1, 2]);       // false
console.log({} === {});               // false

let o1 = {};
let o2 = o1;
console.log(o1 === o2);               // true

两个数组内容相同,但是两个不同对象,地址不同,所以 === 为 false。易错点:不要用 === 直接判断两个对象内容是否相同;NaN === NaN 为 false,可用 Object.is(NaN, NaN);typeof null 是 "object",但 null 是特殊原始值。

用 in 与 hasOwnProperty 区分“能用”和“属于自身” ​

为什么需要?因为引用类型有原型链,一个属性“能用”不代表在对象自身上。怎么做?转写稿中:

js
let arr = [10, 20];
arr.userName = "高考";
arr.__proto__.address = "上海";

console.log("length" in arr);              // true
console.log("push" in arr);                // true
console.log("userName" in arr);            // true
console.log("address" in arr);             // true
console.log("h" in arr);                   // false

console.log(arr.hasOwnProperty("userName")); // true
console.log(arr.hasOwnProperty("address"));  // false
console.log(arr.hasOwnProperty("length"));   // true
console.log(arr.hasOwnProperty("push"));     // false

in 只要自身或原型链上能找到就为 true;hasOwnProperty 只看自身。易错点:hasOwnProperty 本身也可能被覆盖,复杂场景可用 Object.prototype.hasOwnProperty.call(obj, key)。

Object.create 与原型链:创建对象时指定原型 ​

为什么需要?有时我们不想用默认原型,而想创建时就指定原型。怎么做?转写稿演示:

js
let obj1 = {};
console.log(obj1.__proto__ === Object.prototype); // true

let obj2 = Object.create([1, 2, 3]);
console.log(obj2.__proto__); // 数组 [1,2,3]

let obj4 = Object.create(null);
console.log(obj4.__proto__); // undefined,没有原型

易错点:Object.create("hello") 会报错,因为参数必须是对象或 null;需要包装时用 Object.create(new String("hello"))。原型链用于描述属性查找:先自身,再原型,再到顶层 Object.prototype,最后到 null。到顶还没有就返回 undefined,通常不报错。理解这一点,就能明白为什么引用类型既能共享方法,又能在自身上保存不同数据。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例1:数组原型、in 与 hasOwnProperty ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>数组原型与属性判断</title>
</head>
<body>
<script>
  const arr01 = [12, 13, 14, 0];

  // 给数组对象自身添加属性
  arr01.name = '高考';

  // 给数组原型添加属性,会影响所有数组
  arr01.__proto__.age = 100;
  arr01.__proto__.address = '上海';

  console.log('arr01.name:', arr01.name);       // 高考
  console.log('arr01.age:', arr01.age);         // 100
  console.log('arr01.address:', arr01.address); // 上海

  const arr02 = ['A', 'B', 'C'];
  console.log('arr02.name:', arr02.name);       // undefined
  console.log('arr02.age:', arr02.age);         // 100
  console.log('arr02.address:', arr02.address); // 上海

  // 给 arr02 自身赋值 address,不会修改原型
  arr02.address = '北京';

  console.log('arr01.address:', arr01.address); // 上海
  console.log('arr02.address:', arr02.address); // 北京

  console.log('name in arr01:', 'name' in arr01);       // true
  console.log('push in arr01:', 'push' in arr01);       // true
  console.log('address in arr01:', 'address' in arr01); // true
  console.log('h in arr01:', 'h' in arr01);             // false

  console.log('hasOwnProperty name:', arr01.hasOwnProperty('name'));       // true
  console.log('hasOwnProperty age:', arr01.hasOwnProperty('age'));         // false
  console.log('hasOwnProperty length:', arr01.hasOwnProperty('length'));   // true
  console.log('hasOwnProperty push:', arr01.hasOwnProperty('push'));       // false
  console.log('hasOwnProperty address:', arr01.hasOwnProperty('address')); // false
  console.log('arr02 hasOwnProperty address:', arr02.hasOwnProperty('address')); // true
</script>
</body>
</html>

关键行和易错点:

  • arr01.__proto__.age = 100 改的是数组原型,arr02 也能通过原型链读到 age。
  • arr02.address = '北京' 是赋值操作,自身没有 address 时会加到自身,不会改原型。
  • in 会沿原型链查找,所以 push in arr01 为 true。
  • hasOwnProperty 只看对象自身,原型上的 push、address 返回 false。
  • 数组的 length 是自身属性,所以 arr01.hasOwnProperty('length') 为 true。

示例2:自定义构造函数,把方法放到原型上 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>自定义构造函数与原型</title>
</head>
<body>
<script>
  function User(name, age, address) {
    this.name = name;
    this.age = age;
    this.address = address;
  }

  // 方法放到 User.prototype 上,所有实例共享
  User.prototype.addShopCart = function (product) {
    console.log(this.name + '将' + product + '添加到购物车');
  };

  User.prototype.buy = function (product) {
    console.log(this.name + '购买了' + product);
  };

  const u1 = new User('高尔乐', 89, '上海');
  const u2 = new User('贾宝玉', 17, '南京');

  console.log(u1);
  console.log(u2);

  u1.addShopCart('一袋洗衣粉');
  u1.buy('肥皂');

  u2.addShopCart('一袋方便面');
  u2.buy('火腿肠');

  console.log('u1自身有addShopCart吗:', u1.hasOwnProperty('addShopCart')); // false
  console.log('u1自身有name吗:', u1.hasOwnProperty('name'));               // true
  console.log('u1原型是User.prototype吗:', Object.getPrototypeOf(u1) === User.prototype); // true
  console.log('u1和u2原型相同吗:', Object.getPrototypeOf(u1) === Object.getPrototypeOf(u2)); // true
  console.log('u1.constructor是User吗:', u1.constructor === User); // true
  console.log('u1自身有constructor吗:', u1.hasOwnProperty('constructor')); // false
</script>
</body>
</html>

关键行和易错点:

  • this.name = name 等写在构造函数里,会加到每个实例对象自身。
  • addShopCart、buy 挂到 User.prototype 上,所有 User 实例共享,节省内存。
  • 方法里的 this 指向调用它的实例,所以 u1.addShopCart 中 this 是 u1,u2.addShopCart 中 this 是 u2。
  • 如果把方法写在构造函数内部写成 this.addShopCart = function () {},每个实例都会各存一份。
  • u1.hasOwnProperty('addShopCart') 为 false,因为方法在原型上。
  • constructor 来自原型,所以 u1.constructor === User 为 true,但 u1.hasOwnProperty('constructor') 为 false。

示例3:Object.create 创建对象时设置原型与原型链查看 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Object.create 与原型链</title>
</head>
<body>
<script>
  const obj1 = {};
  console.log('obj1原型:', Object.getPrototypeOf(obj1));
  console.log('obj1原型是Object.prototype:', Object.getPrototypeOf(obj1) === Object.prototype);

  const protoArr = ['hello', '小乐'];
  const obj2 = Object.create(protoArr);

  console.log('obj2原型:', Object.getPrototypeOf(obj2));
  console.log('obj2原型是protoArr:', Object.getPrototypeOf(obj2) === protoArr);
  console.log('obj2.length:', obj2.length); // 2
  console.log('obj2[0]:', obj2[0]);         // hello

  // Object.create 参数必须是对象或 null,原始字符串需要包装成对象
  const obj3 = Object.create(new String('hello'));
  console.log('obj3原型:', Object.getPrototypeOf(obj3));
  console.log('obj3原型值:', Object.getPrototypeOf(obj3).valueOf()); // hello

  // 创建没有原型的对象
  const obj4 = Object.create(null);
  console.log('obj4:', obj4);
  console.log('obj4原型:', Object.getPrototypeOf(obj4)); // null
  console.log('toString in obj4:', 'toString' in obj4);  // false

  const arr = [1, 2, 3];
  console.log('arr原型:', Object.getPrototypeOf(arr));
  console.log('arr.__proto__ === Array.prototype:', arr.__proto__ === Array.prototype); // true
  console.log('arr原型是Array.prototype:', Object.getPrototypeOf(arr) === Array.prototype); // true

  console.log('Array.prototype原型:', Object.getPrototypeOf(Array.prototype));
  console.log('Array.prototype原型是Object.prototype:', Object.getPrototypeOf(Array.prototype) === Object.prototype); // true

  console.log('Object.prototype原型:', Object.getPrototypeOf(Object.prototype)); // null
</script>
</body>
</html>

关键行和易错点:

  • Object.create(protoArr) 会在创建对象的同时,把 protoArr 设为新对象的原型。
  • obj2 本身不是数组,但它的原型是数组,所以能继承 length 和索引属性。
  • Object.create('hello') 会报错,因为参数必须是对象或 null;转写稿中使用 new String('hello') 包装成对象。
  • Object.create(null) 创建的是没有原型的对象,没有 toString 等原型方法。
  • 查看原型推荐 Object.getPrototypeOf,__proto__ 仅用于演示,生产代码中不推荐依赖。
  • 原型链查找从对象自身开始,找不到就沿原型向上找,直到 null 结束。

常见错误 ​

  1. 现象:自定义构造函数里写 this.say = function(){},创建 1000 个实例后内存占用明显变大。
    原因:方法写在构造函数体内,每个实例都会复制一份函数。
    修法:实例数据留在 this 上,共享方法移到 Constructor.prototype 上。

  2. 现象:执行 arr2.address = '北京' 后,以为 arr1.address 也会变成北京。
    原因:赋值时若实例自身没有该属性,会在实例自身新增属性,不会修改原型。
    修法:明确区分“改实例”和“改原型”;改原型用 Constructor.prototype.xxx 或 Object.getPrototypeOf(obj)。

  3. 现象:用 in 判断属性是否属于对象本身,结果把原型上的 push 也判成 true。
    原因:in 会检查整条原型链。
    修法:只判断自身属性用 hasOwnProperty 或 Object.hasOwn。

  4. 现象:Object.create('hello') 直接报错。
    原因:Object.create 的参数必须是对象或 null,原始值不会自动包装。
    修法:需要字符串原型时传 new String('hello');需要无原型对象时传 null。

  5. 现象:运行时随意写 obj.__proto__ = xxx 或改共享 prototype,旧实例行为变得混乱。
    原因:改的是原型关联或共享原型对象,影响范围大且难追踪。
    修法:优先用 Object.create(proto) 在创建时指定原型,避免运行时随意改 __proto__。

  6. 现象:{} === {} 得到 false,或 let b = a; b.n = 2 后原对象也被改。
    原因:引用类型比较的是引用地址,赋值复制的是引用。
    修法:值类型按值比较;引用类型按需做浅拷贝/深拷贝,或比较关键字段。

练习 ​

  1. 改错:下面代码创建大量实例时方法重复存储,请改为更合理的写法。

    js
    function User(name) &#123;
      this.name = name;
      this.say = function () &#123;
        console.log(this.name);
      &#125;;
    &#125;
  2. 写出输出并说明赋值发生在哪里。

    js
    function A() &#123;&#125;
    A.prototype.x = 1;
    
    const a = new A();
    a.x = 2;
    
    console.log(a.x, A.prototype.x);
    
    const b = new A();
    console.log(b.x);
  3. 写出输出,并区分 in 与 hasOwnProperty 的判断范围。

    js
    const arr = [];
    arr.name = 'x';
    arr.__proto__.age = 18;
    
    console.log('name' in arr, arr.hasOwnProperty('name'));
    console.log('age' in arr, arr.hasOwnProperty('age'));
    console.log('push' in arr, arr.hasOwnProperty('push'));
  4. 用 Object.create 完成两件事:

    • 创建 obj,让其原型为 { type: 'admin' };
    • 创建一个没有原型的对象 bare。
  5. 写出输出并说明原因。

    js
    let a = &#123; n: 1 &#125;;
    let b = a;
    b.n = 2;
    console.log(a.n);
    
    let c = 1;
    let d = c;
    d = 2;
    console.log(c);

原视频对照 ​

#视频时长
1为什么有些属性放在原型上06:32
2自定义构造函数时原型的应用15:42
3原型题目分析08:03
4判断属性是否属于对象本身05:58
5创建对象的同时设置原型06:46
6原型链23:05

小结 ​

  1. 实例数据放构造函数,共享方法放 prototype。
  2. 读取属性会沿原型链查找,赋值优先落到实例自身。
  3. in 查整条原型链,hasOwnProperty 只查自身。
  4. 原型链终点是 Object.prototype,它的原型是 null。
  5. Object.create(proto) 可在创建时指定原型,传 null 可得无原型对象。
  6. 值类型赋值复制值,引用类型赋值复制引用。
  7. 不要随意改 __proto__ 和共享 prototype,优先用 Object.create 或明确封装。
  8. 构造函数 prototype 与实例 __proto__ 的关系要分清,实例的 constructor 通常来自原型。