原型与继承

简介: 原型与继承

原型与继承


在 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]
相关文章
|
6月前
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(三)
深入JS面向对象(原型-继承)
53 0
|
6月前
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
59 0
用原型链的方式写一个类和子类
用原型链的方式写一个类和子类
34 0
|
6月前
|
设计模式 JavaScript 前端开发
深入JS面向对象(原型-继承)(二)
深入JS面向对象(原型-继承)
65 0
|
6月前
|
JSON JavaScript 前端开发
深入JS面向对象(原型-继承)(四)
深入JS面向对象(原型-继承)
48 0
|
前端开发
前端原型和原型链构造函数的使用
前端原型和原型链构造函数的使用
68 0
|
6月前
|
JavaScript 前端开发
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
145 0
|
6月前
|
设计模式 前端开发 JavaScript
【面试题】 对象、原型、原型链与继承?这次我懂了!
【面试题】 对象、原型、原型链与继承?这次我懂了!
原型链继承: 原理:将父类的实例作为子类的原型
原型链继承: 原理:将父类的实例作为子类的原型
|
6月前
|
设计模式 前端开发 JavaScript
【面试题】对象、原型、原型链与继承 ,你了解多少?
【面试题】对象、原型、原型链与继承 ,你了解多少?