原型与继承
在 JavaScript 中,对象有一个特殊的隐藏属性
[[Prototype]]
(如规范中所命名的),它要么为null
,要么就是对另一个对象的引用。该对象被称为“原型。当我们从
object
中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这被称为“原型继承”。
'use strict';const user = {name: 'user',age: 18
}
const admin = {__proto__: user, // __proto__ 的值可以是对象,也可以是 null。而其他的类型都会被忽略。一个对象只能有一个 [[Prototype]]。一个对象不能从其他两个对象获得继承。name: 'admin',
}
console.log(admin.name); // admin
console.log(admin.age); // 18console.log(admin.__proto__); // { name: 'user', age: 18 }
console.log(admin.__proto__.__proto__); // [Object: null prototype] {}
console.log(admin.__proto__.__proto__.__proto__); // null
// 获取对象所有的属性名, 包括不可迭代的属性
console.log(Object.getOwnPropertyNames(admin.__proto__.__proto__));
/*
['constructor','__defineGetter__','__defineSetter__','hasOwnProperty','__lookupGetter__','__lookupSetter__','isPrototypeOf','propertyIsEnumerable','toString','valueOf','__proto__','toLocaleString'
]
*/
// 只返回自己的 key
console.log(Object.keys(admin)); // [ 'name' ]
// for..in 会遍历自己以及继承的键
for(let key in admin) console.log(key); // name age
// 方法 obj.hasOwnProperty(key):如果 obj 具有自己的(非继承的)名为 key 的属性,则返回 true。
console.log(admin.hasOwnProperty("name")); // true
console.log(admin.hasOwnProperty("age")); // false
__proto__
是[[Prototype]]
的因历史原因而留下来的 getter/setter初学者常犯一个普遍的错误,就是不知道
__proto__
和[[Prototype]]
的区别。请注意,
__proto__
与内部的[[Prototype]]
不一样。__proto__
是[[Prototype]]
的 getter/setter。稍后,我们将看到在什么情况下理解它们很重要,在建立对 JavaScript 语言的理解时,让我们牢记这一点。
__proto__
属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数Object.getPrototypeOf/Object.setPrototypeOf
来取代__proto__
去 get/set 原型。稍后我们将介绍这些函数。根据规范,
__proto__
必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。由于
__proto__
标记在观感上更加明显,所以我们在后面的示例中将使用它。
let user = {setName(name){this.name = name}
}
let admin = {__proto__: user
}
// 即使是调用的继承对象user中的方法, 但是方法中的this依然指向调用者admin
admin.setName("admin");
console.log(admin.name); // admin
console.log(user.name); // undefind
我们还记得,可以使用诸如
new F()
这样的构造函数来创建一个新对象。如果
F.prototype
是一个对象,那么new
操作符会使用它为新对象设置[[Prototype]]
。
"use strict";MyArray.prototype = new Array(); // * 与使用 ** 行, ***行 等效
// Array.prototype = Array.prototype // **
function MyArray() {// this.__proto__ = new Array(); // ***// this 是一个new出来的具体对象, MyArray 是一个构造函数
}const arr = new MyArray();
// MyArray.prototype上的属性方法可直接使用, 未找到会自动向上寻找, 在`arr.__proto__`上找到constructor方法
console.log(arr.constructor === Array); // true
console.log(arr.__proto__.constructor === Array); // true
// 调用Array.prototype 上的方法
arr.push(...[1, 2, 3]);
console.log(arr);
每个函数都有
"prototype"
属性,即使我们没有提供它。默认的
"prototype"
是一个只有属性constructor
的对象,属性constructor
指向函数自身。
"use strict";function User() {}console.log(Object.getOwnPropertyDescriptors(User.prototype));
/* {constructor: {value: [Function: User],writable: true,enumerable: false,configurable: true}} */
console.log(User.prototype.constructor === User); // true
console.log(User.prototype.__proto__.constructor === Object); // true
console.log(User.prototype.__proto__.__proto__ === null); // true, null没有构造方法const user = new User();
console.log(user.__proto__ === User.prototype); // true
console.log(user.__proto__.__proto__.__proto__ === null); // true, 原型链的尽头是null
// User.prototype 上的属性可直接使用
console.log(user.constructor === User); // true
在常规对象上,prototype
没什么特别的:
let user = {name: "John",prototype: "Bla-bla" // 这里只是普通的属性
};
默认情况下,所有函数都有 F.prototype = {constructor:F}
,所以我们可以通过访问它的 "constructor"
属性来获取一个对象的构造器。
"use strict";
function User(name){this.name = name;
}
// 构造方法应加上关键字new调用
let user1 = new User("User1");
console.log(user1);let user2 = new User.prototype.constructor("User2");
console.log(user2);let user3 = new user1.__proto__.constructor("User3");
console.log(user3);let user4 = new user1.constructor("User4");
console.log(user4); // User { name: 'User4' }User.prototype = {}
let user5 = new user1.constructor("User5");
console.log(user5); // { name: 'User5' } 是 new Object('User5')的结果, 继续向上找constructoruser1.__proto__ = null // user1.constructor is not a constructor, 如果给{}, 结果是 [String: 'User6']
let user6 = new user1.constructor("User6");
console.log(user6);
内建对象的原型挂载着相关属性和方法
可向prototype
上添加自定义方法或属性
// 通过原型查看数组, 字符串等相关的函数(可在浏览器环境下直接使用String.prototype)// console.log(Object.getOwnPropertyDescriptors(Array.prototype));
console.log(Object.getOwnPropertyNames(Array.prototype));
/* ['length', 'constructor', 'concat','copyWithin', 'fill', 'find','findIndex', 'lastIndexOf', 'pop','push', 'reverse', 'shift','unshift', 'slice', 'sort','splice', 'includes', 'indexOf','join', 'keys', 'entries','values', 'forEach', 'filter','flat', 'flatMap', 'map','every', 'some', 'reduce','reduceRight', 'toLocaleString', 'toString','at'
] */
// 所有函数或属性名
console.log(Object.getOwnPropertyNames(String.prototype));
/* ['length', 'constructor', 'anchor','big', 'blink', 'bold','charAt', 'charCodeAt', 'codePointAt','concat', 'endsWith', 'fontcolor','fontsize', 'fixed', 'includes','indexOf', 'italics', 'lastIndexOf','link', 'localeCompare', 'match','matchAll', 'normalize', 'padEnd','padStart', 'repeat', 'replace','replaceAll', 'search', 'slice','small', 'split', 'strike','sub', 'substr', 'substring','sup', 'startsWith', 'toString','trim', 'trimStart', 'trimLeft','trimEnd', 'trimRight', 'toLocaleLowerCase','toLocaleUpperCase', 'toLowerCase', 'toUpperCase','valueOf', 'at'
] */
console.log(Object.getOwnPropertyNames(Number.prototype));
/* ['constructor','toExponential','toFixed','toPrecision','toString','valueOf','toLocaleString'
] */
console.log(Object.getOwnPropertyNames(Boolean.prototype));
/* [ 'constructor', 'toString', 'valueOf' ] */console.log(Object.getOwnPropertyNames(Date.prototype));
/* ['constructor', 'toString', 'toDateString','toTimeString', 'toISOString', 'toUTCString','toGMTString', 'getDate', 'setDate','getDay', 'getFullYear', 'setFullYear','getHours', 'setHours', 'getMilliseconds','setMilliseconds', 'getMinutes', 'setMinutes','getMonth', 'setMonth', 'getSeconds','setSeconds', 'getTime', 'setTime','getTimezoneOffset', 'getUTCDate', 'setUTCDate','getUTCDay', 'getUTCFullYear', 'setUTCFullYear','getUTCHours', 'setUTCHours', 'getUTCMilliseconds','setUTCMilliseconds', 'getUTCMinutes', 'setUTCMinutes','getUTCMonth', 'setUTCMonth', 'getUTCSeconds','setUTCSeconds', 'valueOf', 'getYear','setYear', 'toJSON', 'toLocaleString','toLocaleDateString', 'toLocaleTimeString'
] */
console.log(Object.getOwnPropertyNames(Map.prototype));
/* ['constructor', 'get','set', 'has','delete', 'clear','entries', 'forEach','keys', 'size','values'
] */
console.log(Object.getOwnPropertyNames(WeakMap.prototype));
/* [ 'constructor', 'delete', 'get', 'set', 'has' ] */console.log(Object.getOwnPropertyNames(Function.prototype));
/* ['length', 'name','arguments', 'caller','constructor', 'apply','bind', 'call','toString'
] */
console.log(Object.getOwnPropertyNames(Object.prototype));
/* ['constructor','__defineGetter__','__defineSetter__','hasOwnProperty','__lookupGetter__','__lookupSetter__','isPrototypeOf','propertyIsEnumerable','toString','valueOf','__proto__','toLocaleString'
] */
console.log(Object.getOwnPropertyNames(Object));
/* ['length','name','prototype','assign','getOwnPropertyDescriptor','getOwnPropertyDescriptors','getOwnPropertyNames','getOwnPropertySymbols','is','preventExtensions','seal','create','defineProperties','defineProperty','freeze','getPrototypeOf','setPrototypeOf','isExtensible','isFrozen','isSealed','keys','entries','fromEntries','values','hasOwn'
] */
如果速度很重要,就请不要修改已存在的对象的
[[Prototype]]
通常我们只在创建对象的时候设置它一次,自那之后不再修改
现代的获取/设置原型的方法有:
- Object.getPrototypeOf(obj) —— 返回对象
obj
的[[Prototype]]
。- Object.setPrototypeOf(obj, proto) —— 将对象
obj
的[[Prototype]]
设置为proto
。
Object.create(null)
这样的对象称为 “very plain” 或 “pure dictionary” 对象,因为它们甚至比通常的普通对象(plain object){...}
还要简单。缺点是这样的对象没有任何内建的对象的方法,例如
toString
:
'use strict';
// 这个对象没有原型([[Prototype]] 是 null)
// 创建一个以null为原型的对象, 在obj中, __ptoto__、toString等原本的内建属性和方法不复存在, 只是普通的方法和属性
let obj = Object.create(null);console.log(obj.__proto__); // undefined
// console.log(obj.toString()); // TypeError: obj.toString is not a function// 正常的
let obj1 = {};
console.log(obj1.__proto__); // [Object: null prototype] {}
console.log(obj1.toString()); // [object Object]