MDN学习-JavaScript篇--原型

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。

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

注意: 理解对象的原型(可以通过Object.getPrototypeOf(obj)或者已被弃用的__proto__属性获得)与构造函数的prototype属性之间的区别是很重要的。前者是每个实例上都有的属性,后者是构造函数的属性。也就是说,Object.getPrototypeOf(new Foobar())Foobar.prototype指向着同一个对象。

function Person(first,last,age,gender,interests) {
    this.name = {
        'first': first,
        'last': last
    };
    this.age = age;
    this.gender = gender;
    this.interests = interests;
    this.ios = function() {
        console.log(`${this.name.first}'  '${this.name.last} is ${this.age} years old likes ${this.interests[0]} and ${this.interests[1]}.`)
    };
    this.greeting = function(){
        console.log(`My name is ${this.name.first}`)
    }
}
var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);

 

prototype 属性:继承成员被定义的地方

那么,那些继承的属性和方法在哪儿定义呢?如果你查看  Object  参考页,会发现左侧列出许多属性和方法——大大超过我们在  person1  对象中看到的继承成员的数量。某些属性或方法被继承了,而另一些没有——为什么呢?

原因在于,继承的属性和方法是定义在  prototype  属性之上的(你可以称之为子命名空间 (sub namespace) )——那些以  Object.prototype. 开头的属性,而非仅仅以  Object.  开头的属性。 prototype  属性的值是一个对象,我们希望被原型链下游的对象继承的属性和方法,都被储存在其中。

于是  Object.prototype.watch() 、 Object.prototype.valueOf()  等等成员,适用于任何继承自  Object()  的对象类型,包括使用构造器创建的新的对象实例。

 Object.is() 、 Object.keys() ,以及其他不在  prototype  对象内的成员,不会被“对象实例”或“继承自  Object()  的对象类型”所继承。这些方法/属性仅能被  Object()  构造器自身使用。

你可以检查已有的 prototype 属性。回到先前的例子,在 JavaScript 控制台输入:

Person.prototype

输出并不多,毕竟我们没有为自定义构造器的原型定义任何成员。缺省状态下,构造器的  prototype  属性初始为空白。现在尝试:

Object.prototype

你会看到  Object  的  prototype  属性上定义了大量的方法;如前所示,继承自  Object  的对象都可以使用这些方法。

JavaScript 中到处都是通过原型链继承的例子。比如,你可以尝试从  String 、 Date 、 Number 和  Array  

var myString = 'This is my string.';

 myString  立即具有了一些有用的方法,如  split() 、 indexOf() 、 replace()  等。

全局对象的原型中寻找方法和属性。它们都在原型上定义了一些方法,因此当你创建一个字符串时:

create()

1、例如,在上个例子的 JavaScript 控制台中输入:

var person2 = Object.create(person1);

2、create() 实际做的是从指定原型对象创建一个新的对象。这里以 person1 为原型对象创建了 person2 对象。在控制台输入:

person2.__proto__

结果返回对象person1

constructor 属性

每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。

例如,在控制台中尝试下面的指令:

person1.constructor
person2.constructor

都将返回 Person() 构造器,因为该构造器包含这些实例的原始定义。

一个小技巧是,你可以在  constructor  属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。

在控制台中输入:

var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);

现在尝试访问新建对象的属性,例如:

person3.name.first
person3.age
person3.bio()

正常工作。通常你不会去用这种方法创建新的实例;但如果你刚好因为某些原因没有原始构造器的引用,那么这种方法就很有用了。

此外, constructor  属性还有其他用途。比如,想要获得某个对象实例的构造器的名字,可以这么用:

instanceName.constructor.name

具体地,像这样:

person1.constructor.name

修改原型

从我们从下面这个例子来看一下如何修改构造器的 prototype 属性。

这段代码将为构造器的 prototype 属性添加一个新的方法:

Person.prototype.farewell = function() {
  alert(this.name.first + ' has left the building. Bye for now!');
}

保存代码,在浏览器中加载页面,然后在控制台输入:

person1.farewell();

你会看到一条警告信息,其中还显示了构造器中定义的人名;这很有用。但更关键的是,整条继承链动态地更新了,任何由此构造器创建的对象实例都自动获得了这个方法。

再想一想这个过程。我们的代码中定义了构造器,然后用这个构造器创建了一个对象实例,此后向构造器的  prototype  添加了一个新的方法:

function Person(first, last, age, gender, interests) {

  // 属性与方法定义

};

var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);

Person.prototype.farewell = function() {
  alert(this.name.first + ' has left the building. Bye for now!');
}

但是  farewell()  方法仍然可用于  person1  对象实例——旧有对象实例的可用功能被自动更新了。这证明了先前描述的原型链模型。这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。

你很少看到属性定义在 prototype 属性中,因为如此定义不够灵活。比如,你可以添加一个属性:

Person.prototype.fullName = 'Bob Smith';

但这不够灵活,因为人们可能不叫这个名字。用 name.first 和 name.last 组成 fullName会好很多

Person.prototype.fullName = this.name.first + ' ' + this.name.last;

然而,这么做是无效的,因为本例中 this 引用全局范围,而非函数范围。访问这个属性只会得到 undefined undefined。但这个语句若放在 先前定义在 prototype 上的方法中则有效,因为此时语句位于函数范围内,从而能够成功地转换为对象实例范围。你可能会在 prototype 上定义常属性 (constant property) (指那些你永远无需改变的属性),但一般来说,在构造器内定义属性更好。

事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:

// 构造器及其属性定义

function Test(a,b,c,d) {
  // 属性定义
};

// 定义第一个方法

Test.prototype.x = function () { ... }

// 定义第二个方法

Test.prototype.y = function () { ... }

// 等等……

本文介绍了 JavaScript 对象原型,包括原型链如何允许对象之间继承特性、prototype 属性、如何通过它来向构造器添加方法,以及其他有关主题。

下一篇文章中,我们将了解如何在两个自定义的对象间实现功能的继承。

posted @ 2019-08-23 13:20  格雷迪  阅读(513)  评论(0)    收藏  举报