原型与继承
在 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); // 18 console.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')的结果, 继续向上找constructor user1.__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]