一、什么是原型?
在Javascript中,函数可以拥有属性,也可以给函数附加属性。每个函数都有一个特性的属性叫做原型(prototype)。原型这个属性本身是对象类型的,每个原型对象默认包含两个属性:constructor 和 __proto__;其中constructor属性是当前函数的构造器,它可被用于构造当前函数的实例,而__proto__是一个特性属性,它指向构建当前函数的原型(prototype)。
如果我们定义一个函数func1,语法如下:
function func1() {};
这个时候我们可以通过func1.prototype来访问函数func1的原型对象(prototype)。同理,我们还可以给这个原型对象附加属性:
function func1() {};
func1.prototype.funcName = 'myFunc1';
console.log(func1.prototype);
输出结果:
{funcName: "myFunc1", constructor: ƒ}
funcName: "myFunc1"
constructor: ƒ func1()
__proto__: Object
集合上面的解释和代码示例,我们可以简单的总结得到几个关键知识点。
关键知识点
- 每个函数都有原型(
prototype); - 原型本身是一个对象,可以通过函数名.prototype来访问;
- 原型本身有2个默认属性:
constructor和__proto__,其中constructor是当前函数的构造器,可以用于构造当前函数的实例对象;__proto__是一个特性属性,它指向构建当前函数的原型(prototype); - 函数的原型对象可以附加一些属性;
扩展问题
这里,读者可以暂停一下,读记一下上面的关键点。我向读者提出几个问题,读者可以思考一下,带着问题继续往下看。问题是:
- 每个函数都有原型,那么拥有原型的对象是不是函数?
不知道读者心中是否有答案,这里我来解析一下这个问题。
首先,我们可以通过 instanceof关键字来判断一个对象是否是由另一个对象构造的实例。所以我们可以通过语法:Obj instanceof Function来判断某个对象是否为Function的实例,即某个对象是不是函数(因为函数都是Function的实例)。
举个例子:
function func1(){};
func1 instanceof Function; // true
// Function 本身也是一个函数
Function instanceof Function; // true
// 所有对象都是Object的实例,但是Object是不是函数呢?
Object instanceof Function; // true
// 试一下其他的内置对象
String instanceof Function; // true
Number instanceof Function; // true
// 试一下ES6的新语法class,一个类是不是函数呢?
class SomeClass{};
SomeClass instanceof Function; // true
读者可以在控制台打印一下上面的func1、Function、Object、String、Number、SomeClass,可以发现它们都拥有原型属性(prototype)。而且它们都可以通过new关键字来实例化,结合我们了解到的原型本身拥有构造器(constructor)属性,就可以得出结论:
- 拥有原型的对象本质上都是函数;
- 拥有原型的对象可以被实例化,是通过构造器(
constructor)属性来实例化的;
二、什么是原型链?
Javascript中,每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推,这种关系常被称为原型链 (prototype chain)。
读完解释,读者有没有些许疑问:原型链到底是个什么东西?原型链起了什么作用?(建议思考30秒)
原型链是一种链式关系。实际上是原型对象和该原型对象的原型构成的一种链式关系,这种关系被称为原型链;其中原型对象可以继承该原型对象的原型的属性和方法。 简而言之,原型链表示了原型对象和该原型对象的原型之间的一种链式关系,但是本质上还是因为原型对象和原型对象的原型之间有继承关系。所以,原型链实际上是表示JavaScript语言中通过原型实现的继承关系,这种继承关系是链式的,故而被称为原型链。举例来说:
function Dog(){};
Dog.prototype.personalName= 'A Dog';
Dog.prototype.bark = () => console.log('wang...');
const wangcai = new Dog();
const erHa = new Dog();
console.log(wangcai.personalName); // 'A Dog'
console.log(wangcai.bark()); // 'wang...'
console.log(erHa.personalName); // 'A Dog'
wangcai.personalName = 'wangcai';
wangcai.bark = () => console.log('旺旺雪饼');
console.log(wangcai.personalName); // 'wangcai'
console.log(wangcai.bark()); // '旺旺雪饼'
可以看到,代码中先定义了一个函数Dog,然后在函数Dog的原型(prototype)上定义了属性personalName和方法bark,而后使用关键字new实例化得到了 2个对象wangcai(旺财)erHa(二哈)。可以看到,旺财和二哈都能使用函数Dog的属性PersonalName和方法bark,但是旺财和二哈的名字都是A Dog(一只狗),都只会叫wang...。
这时候,代码中给旺财取了新的名字(wangcai)并且有了自己独特的叫声(旺旺雪饼),再次访问旺财的属性personalName和方法bark时,访问的是新赋予的属性和方法。
理性分析一下,可以看到,wangcai继承了函数Dog定义在它的原型(prototype)上的属性(personalName)方法(bark)。即通过原型,实现了继承,而对wangcai继承的属性和方法进行重新赋值,实现了属性和方法的重载;也就是说,利用原型(prototype)的特性,实现了面向对象。所以,也可以说Javascript是基于原型实现的面向对象。
总结一下上面文字和代码示例,可以得到以下几个关键知识点。
关键知识点
- Javascript基于原型实现了继承,基于原型实现了面向对象,JavaScript的面向对象是基于原型的面向对象(区别于其他基于类的面向对象的语言,如:Java等);
- 在函数的原型对象(
prototype)上附加的属性和方法可以被该函数的实例对象继承和重载;
扩展问题
读到这里,读者或许会想:
- 函数的实例到底是怎么继承函数的额属性和方法的?
- 这其中是怎么样的一种机制?
我们可以在控制台上输入以下代码:
Dog.prototype;
erHa.__proto__;
得到的输出都是:
{personalName: "A Dog", bark: ƒ, constructor: ƒ}
bark: () => console.log('wang...')
personalName: "A Dog"
constructor: ƒ Dog()
__proto__: Object
如果在控制台输入以下代码:
erHa.__proto__ === Dog.prototype; // true
Dog.__proto__ === Function.prototype; // true
Dog.prototype.__proto__ === Object.prototype; // true
通过上方的代码,可以总结出以下结论:
- 对象的
__proto__属性指向了构造它的对象的原型(prototype); - 函数的原型(
prototype)的__proto__属性指向了构造它的原型即Object.prototype; Object的原型(prototype)的__proto__属性指向了null;
这3个结论虽然简短,但不那么容易理解,我来解析一下。
第一个结论,对象的__proto__属性指向了构造它的对象的原型(prototype);
举个例子,我们知道对象erHa是由对象Dog(函数本身也是对象)构造出来的,而Dog本身又是由Function构造出来的(所有函数都是由Function构造出来的),根据上面的第一行和第二行代码得到印证。我们还可以举例子佐证以下,我们知道Javascript中有一些内置的特定对象,它们本身也是函数可以被实例化(例如: String、Number、Object等),然后得到对应的对象。那么它们既然都是函数,就都应该是由Function构造的,所以它们的__proto__属性应该指向Function的原型(prototype)。用代码验证一下:
String.__proto__ === Function.prototype; // true
Number.__proto__ === Function.prototype; // true
Object.__proto__ === Function.prototype; // true
可以看到,我们的想法得到验证。那么,有些读者可能会想到,Function本身就是一个函数,那么按照第一个结论,Function应该是由Function构造的(自己生自己啊)。 Function的属性__proto__应该指向的是的是Function的原型(prototype)。读者可以在控制台输入以下代码,能够验证这个猜想是正确的。
Function.__proto__ === Function.prototype; // true
经过多重举证,可以的出第一个结论是正确的。那么,我们继续分析一下第二个结论:
函数的原型(prototype)的__proto__属性指向了构造它的原型即Object.prototype。
上面的例子中,我们定义了函数Dog,String、Number、Function、Object都是函数。按照上面的结论,这几个函数的原型(prototype)的__proto__属性应该都指向Object的原型(prototype)。在控制台输入以下代码:
Dog.prototype.__proto__ === Object.prototype; // true
String.prototype.__proto__ === Object.prototype; // true
Number.prototype.__proto__ === Object.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
// 注意
Object.prototype.__proto__ === Object.prototype; // false
Object.prototype.__proto__ === null; // true
可以看到作为函数的Dog,String、Number、Function的原型(prototype)的__proto__属性都指向了Object的原型(prototype),印证了结论二。而只有Object的原型(prototype)的__proto__属性都指向了null,印证了结论三。
结合上面的结论,我们回顾一下最开始的例子。在控制台输入以下代码:
wangcai.__proto__ === Dog.prototype;
Dog.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;
wangcai.__proto__.__proto__.__proto__ === null;
可以看到,wangcai、Dog.prototype、Object.prototype、null之间构成了一条链式关系。那么,如果我们在对象Object的原型(prototype)上定义的属性就会被一级级向下继承,所以wangcai会继承Dog和Object定义在它们的原型(prototype)上的属性和方法。
我们把想要定义的属性定义在对象原型链的上级上,对象就可以继承这些属性和方法了。那么,有没有读者会这样想:既然函数的最终都会继承Object的原型(prototype)上的属性和方法,那么只要把想要继承的属性方法都定义在Obejct上不是很方便?
这个想法很不错,但是我们不应该这么做。不同于__基于类面向对象__的方式,在实例化的时候把父类的属性和方法拷贝给了实例化的对象本身,Javascript中并没有在实例化的时候进行拷贝操作。而是当访问实例化的对象的时候,比如erHa,对象erHa上并没有定义属性personalName和方法bark,就会沿着原型链向上,在Dog的原型上寻找属性personalName和方法bark,如果没有就会继续验证原型链寻找,若最终没有找到才会返回undefined。可以看到,如果我们把所有想要被继承的属性放到Object的原型(prototype)上,那么访问这些属性的时候就会验证原型链一直向上寻找,会更消耗性能。
所以在实际开发中,应该把想要被继承的属性和方法按照原型链一级级定义。
三、扩展内容:class与原型
阅读了上面的文章,读者可以知道,class只是一种语法糖,其本质也是通过原型实现的继承,这里我来和读者简单的探讨下class中的原型。 举例说明:
class Person {}
class Man extends {}
var man1 = new Man();
Person.__proto__ === Function.prototype;
Man.__proto__ === Person;
man1.__proto__ === Man.prototype;
这里我们发现类Man的原型对象实际上就是Person本身,而不是Person.prototype。所以当我们给Person对象本身直接添加属性的时候,Man这个实例就能够通过原型链继承Person上添加上的属性。而当我们给Person.prototype添加属性的时候,Man并不能通过原型链实现继承。代码如下:
class Person {}
class Person {};
class Man extends Person {};
var man1 = new Man();
Person.__proto__ === Function.prototype;
Man.__proto__ === Person;
man1.__proto__ === Man.prototype;
Person.className = 'Person';
Person.prototype.myClassName = 'MyPerson';
console.log('Man.className', Man.className);
// 输出:Man.className Person
console.log('Man.myClassName', Man.myClassName);
// 输出:Man.myClassName undefined
可以看到,当我们给Person对象上添加了属性className的时候,Man.className可以通过原型链访问到;但是,当我们给Person.prototype添加属性myClassName的时候,Man.myClassName的值是undefined。这样我们可以得出一个结论:class中的继承实际上子类的原型对象是父类本身,而不是父类的prototype。
这样,我们可以简单的模拟一下class,代码如下:
function Animal() {};
function Dog() {};
Dog.__proto__ = Animal;
Animal.className = "Animal";
console.log("Dog.className", Dog.className);
// 输出:Dog.className Animal
class Person {};
class Man extends Person {};
上面的代码中,我们在Animal上直接添加了属性className,对象Dog也继承了该属性,可以通过Dog.className直接访问到。 这里只是简单的引申一下,如果读者对class与原型的关系有兴趣,建议翻阅官方文档或者通过搜索引擎查阅相关资料。