主题
原型与原型链(Day9 2/3)
本页对应 Day09_JavaScript,共 6 段视频,约 1 小时 6 分。
学习目标
- 能解释将实例共享的方法放在原型上的原因,并说明其节省内存空间的机制。
- 能使用自定义构造函数创建实例,并将需要共享的方法正确添加到构造函数的
prototype上。 - 能判断给定属性位于实例自身还是原型上,并正确区分
in与hasOwnProperty的判断结果。 - 能分析对象属性读取与赋值时的查找规则,说明给实例属性赋值为何不会直接修改原型上的同名属性。
- 能使用
Object.create在创建对象时指定原型,或创建没有原型的对象,并验证其原型关系。 - 能描述原型链的属性查找过程,判断对象原型链的终点,并能结合值类型与引用类型说明赋值和比较时的行为差异。
知识点
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")); // falsein 只要自身或原型链上能找到就为 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:数组原型、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结束。
常见错误
现象:自定义构造函数里写
this.say = function(){},创建 1000 个实例后内存占用明显变大。
原因:方法写在构造函数体内,每个实例都会复制一份函数。
修法:实例数据留在this上,共享方法移到Constructor.prototype上。现象:执行
arr2.address = '北京'后,以为arr1.address也会变成北京。
原因:赋值时若实例自身没有该属性,会在实例自身新增属性,不会修改原型。
修法:明确区分“改实例”和“改原型”;改原型用Constructor.prototype.xxx或Object.getPrototypeOf(obj)。现象:用
in判断属性是否属于对象本身,结果把原型上的push也判成true。
原因:in会检查整条原型链。
修法:只判断自身属性用hasOwnProperty或Object.hasOwn。现象:
Object.create('hello')直接报错。
原因:Object.create的参数必须是对象或null,原始值不会自动包装。
修法:需要字符串原型时传new String('hello');需要无原型对象时传null。现象:运行时随意写
obj.__proto__ = xxx或改共享prototype,旧实例行为变得混乱。
原因:改的是原型关联或共享原型对象,影响范围大且难追踪。
修法:优先用Object.create(proto)在创建时指定原型,避免运行时随意改__proto__。现象:
{} === {}得到false,或let b = a; b.n = 2后原对象也被改。
原因:引用类型比较的是引用地址,赋值复制的是引用。
修法:值类型按值比较;引用类型按需做浅拷贝/深拷贝,或比较关键字段。
练习
改错:下面代码创建大量实例时方法重复存储,请改为更合理的写法。
jsfunction User(name) { this.name = name; this.say = function () { console.log(this.name); }; }写出输出并说明赋值发生在哪里。
jsfunction A() {} 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);写出输出,并区分
in与hasOwnProperty的判断范围。jsconst 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'));用
Object.create完成两件事:- 创建
obj,让其原型为{ type: 'admin' }; - 创建一个没有原型的对象
bare。
- 创建
写出输出并说明原因。
jslet a = { n: 1 }; 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 |
小结
- 实例数据放构造函数,共享方法放
prototype。 - 读取属性会沿原型链查找,赋值优先落到实例自身。
in查整条原型链,hasOwnProperty只查自身。- 原型链终点是
Object.prototype,它的原型是null。 Object.create(proto)可在创建时指定原型,传null可得无原型对象。- 值类型赋值复制值,引用类型赋值复制引用。
- 不要随意改
__proto__和共享prototype,优先用Object.create或明确封装。 - 构造函数
prototype与实例__proto__的关系要分清,实例的constructor通常来自原型。