当前位置:首页 / 编程技术 / 后端技术 / 一图了解Javascript原型和原型链
一图了解Javascript原型和原型链
发布时间:2022/07/09来源:51CTO

原型和原型链

JavaScript 常被描述为一种基于原型的语言 ——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain) ,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。更准确地说,这些属性和方法定义在对象的构造器函数的prototype属性上,而非对象实例本身。

一图了解Javascript原型和原型链

在传统的 OOP 中,首先定义“类”,然后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(__proto__​属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

那么,到底什么是原型呢?让我们从对象的属性开始讲起!

在JavaScript世界中,万物皆对象,可以说,JS对象是动态的属性“包”(指自己的属性)。

// 从一个函数里创建一个对象 f,它自身拥有属性 a 和 b :
let Foo = function () {
this.a = 1;
this.b = 2;
this.doSomthng = function() {
console.log('hello');
}
}
/* 这么写也一样
function Foo() {
this.a = 1;
this.b = 2;
this.doSomthng = function() {
console.log('hello, f');
}
}
*/
let f = new Foo(); // { a: 1, b : 2 }

// 我们也可以为函数添加属性
Foo.c = 3
Foo.doSomething = function() {
console.log('hello, Foo');
}

如何能知道对象有哪些属性呢?后续写一篇专门讨论对象的属性,这里我们用Object.getOwnPropertyNames来获取对象自身的属性:

// 一个对象自己的属性是指直接对该对象定义的属性,而不是从该对象的原型继承的属性。
// 对象的属性包括字段(对象)和函数。
// `Object.getOwnPropertyNames`方法同时返回可枚举的和不可枚举的属性和方法的名称。

Object.getOwnPropertyNames(f) // ['a', 'b', 'doSomthng']
Object.getOwnPropertyNames(Foo) // ['length', 'name', 'arguments', 'caller', 'prototype', 'c', 'doSomething']

这里的c,doSomething​是我们手动为Foo添加的,其余的是Foo函数创建之初自带的属性。注意到Foo的属性中有一个prototype原型属性,这就是我们将要讨论到的函数原型对象。

原型 prototype​ 和 __proto__

在JavaScript中,每个实例对象都有一个私有属性(称之为[[Prototype]]​,等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__​。函数(function)在JS中也是对象,是允许拥有属性的。所有的函数会有一个特别的属性 —— prototype​ 。继承的属性和方法是定义在 prototype 属性之上的。

function doSomething(){}
console.log( doSomething.prototype );
// 和声明函数的方式无关,JavaScript 中的函数永远有一个默认原型属性。
var doSomething = function(){};
console.log( doSomething.prototype );

/*
{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf(),
__proto__: null
}
}
*/

我们可以给 doSomething 函数的原型对象添加新属性,并通过 new 操作符来创建基于这个原型对象的 doSomething 实例对象。如下:

doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );

/*
{
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf(),
__proto__: null
}
}
*/


var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // 在实例对象中添加属性
console.log( doSomeInstancing );
console.log( doSomeInstancing.foo ); // 'bar'

/*
{
prop: "some value",
__proto__: {
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf(),
__proto__: null
}
}
}
*/

如上所示, doSomeInstancing​ 中的__proto__​是 doSomething.prototype​,而且doSomeInstancing​继承到了doSomething的foo属性。

原型链 prototype chain

其实,每个实例对象的__proto__​属性指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__​),层层向上直到一个对象的原型对象为 null​。根据定义,null​ 没有原型,并作为这个原型链中的最后一个环节。JavaScript 中几乎所有对象都是位于原型链顶端的 ​Object 的实例。

proto (2).png

为了更清晰地理解,这里将原型相关的内容画了一张图。图中从任意一个对象出发,都可以顺着原型属性 __proto__​ 找到一条原型链,对象属性的查找正是遵循原型链的(下面会以constructor属性为例)。

// f --> Foo.prototype --> Object.prototype --> null
f.__proto__ === Foo.prototype // true
f.__proto__.__proto__ === Object.prototype // true
f.__proto__.__proto__.__proto__ === null // true

// F --> Function.prototype --> Object.prototype --> null
F.__proto__ === Function.prototype // true
F.__proto__.__proto__ === Object.prototype // true
F.__proto__.__proto__.__proto__ === null // true

// o --> Object.prototype --> null // true
o.__proto__ === Object.prototype // true
o.__proto__.__proto__ === null // true

// 扩展一下:
// Array --> Function.prototype --> Object.prototype --> null
Array.__proto__ === Function.prototype // true
Array.__proto__.__proto__ === Object.prototype // true
Array.__proto__.__proto__.__proto__ === null // true

从上图我们可以得知,prototype​对象有个constructor​对象,指向原构造函数。函数的原型对象和函数本身通过prototype​属性和constructor​属性形成一个循环引用。

Foo.prototype.constructor === Foo // true
Function.prototype.constructor === Function // true
Object.prototype.constructor === Object // true

既然原型对象有constructor属性,那普通对象有吗?

Object.getOwnPropertyNames(Foo.prototype) // ['constructor']
Object.getOwnPropertyNames(f) // ['a', 'b', 'doSomthng']

f.constructor
/*
ƒ () {
this.a = 1;
this.b = 2;
this.doSomthng = function() {
console.log('hello');
}
}
*/

咋一看,普通对象 f​ 并没有自己的constructor​属性,但是如果你尝试访问它,你可以得到一个结果,这不就是我们定义的函数 Foo​!确实,f是Foo​函数通过new​构造出来的,但是f​并没有属于自己的constructor​属性,我们看到的结果是f​从它的原型链中获取到的(这就是图中用虚线表示constructor​关系的原因)。函数对象的constructor同理!

f.constructor === f.__proto__.constructor // true
f.constructor === Foo // true

Object.constructor === Object.__proto__.constructor // true
Object.constructor === Function // true

Function.constructor === Function.__proto__.constructor // true
Function.constructor === Function // true

JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾,直到最后还未找相应属性,则断言该属性不存在,并给出属性值为 undefined 的结论。

prototype​ 和 Object.getPrototypeOf:

prototype​ 是用于类(函数)的,而 Object.getPrototypeOf() 是用于实例的(instances),两者功能一致。

function A() {}
A.prototype.doSomething = function () {
console.log('A')
}
a1 = new A()
a1.doSomething()
//执行 `a1.doSomething()` 相当于执行
A.prototype.doSomething.call(a1)
Object.getPrototypeOf(a1).doSomething.call(a1)

instanceof 运算符

instanceof​ 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

function Foo() { } ;
var f1 = new Foo();
f1.__proto__ === Foo.prototype; // true

特别地:

Object instanceof Object // true
Function instanceof Function // true
Object instanceof Function // true
Function instanceof Object // true

因为:

Object.__proto__.__proto__ === Object.prototype //true 
Function.__proto__ === Function.prototype //true
Object.__proto__ === Function.prototype //true
Function.__proto__.__proto__ === Object.prototype //true

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype​ 继承的 hasOwnProperty 方法。

不同方式创建对象和生成原型链

1. 使用语法结构创建的对象


let o = {a: 1};
// o 继承了 Object.prototype 上面的所有属性
// o 自身没有名为 hasOwnProperty 的属性
// hasOwnProperty 是 Object.prototype 的属性
// 因此 o 继承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型为 null
// 原型链如下:
// o ---> Object.prototype ---> null


var a = [1, 2, 3];
// 数组都继承于 Array.prototype
// (Array.prototype 中包含 indexOf, forEach 等方法)
// 原型链如下:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
return 2;
}
// 函数都继承于 Function.prototype
// (Function.prototype 中包含 call, bind 等方法)
// 原型链如下:
// f ---> Function.prototype ---> Object.prototype ---> null

2. 使用构造器创建的对象

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符来作用这个函数时,它就可以被称为构造方法(构造函数)。

function Graph() {
this.vertices = [];
this.edges = [];
}

Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};

var g = new Graph();
// g 是生成的对象,他的自身属性有 'vertices' 和 'edges'。
// 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。
// g 继承了addVertex方法

new 关键字会进行如下的操作:

  • 创建一个空的简单 JavaScript 对象(即{});
  • 为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  • 将步骤 1 新创建的对象作为this的上下文 ;
  • 如果该函数没有返回对象,则返回this。
function myNew(Fn, ...args) {
const obj = {}
obj.__proto__ = Fn.prototype
// const obj = Object.create(Fn.prototype);
const res = Fn.apply(obj, args);
return res instanceof Object ? res : obj
}

3. 使用 Object.create 创建的对象

ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数。

var a = {a: 1};
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined,因为 d 没有继承 Object.prototype

4. 使用 class 关键字创建的对象

ECMAScript6 引入了一套新的关键字用来实现 class​。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 class​, constructor​, static​, extends​, super。class不过是ES6的语法糖罢了。

"use strict";

class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
}

class Square extends Polygon {
constructor(sideLength) {
super(sideLength, sideLength);
}
get area() {
return this.height * this.width;
}
set sideLength(newLength) {
this.height = newLength;
this.width = newLength;
}
}

var square = new Square(2);

总结

原型链的内容是比较难理解的,里面的概念容易混淆,牢记以下几点,对着前面的图边写代码边梳理关系,相信你也很快就能搞定JS的原型和原型链!

  • 要搞清楚__proto__、prototype、constructor 三者的关系
  • 要知道JS中对象和函数的关系,函数其实是对象的一种。
  • __proto__、constructor​属性是对象所独有的;prototype​属性是函数独有的;函数也是对象的一种,所以函数同样也有属性__proto__、constructor
分享到:
免责声明:本文仅代表文章作者的个人观点,与本站无关,请读者仅作参考,并自行核实相关内容。文章内容来源于网络,版权归原作者所有,如有侵权请与我们联系,我们将及时删除。
资讯推荐
热门最新
精品工具
全部评论(0)
剩余输入数量90/90
暂无任何评论,欢迎留下你的想法
你可能感兴趣的资讯
换一批