JavaScript中的对象(二):原型与原型链

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_44196299/article/details/100145637

在我的博客JavaScript中的对象(一):面向对象中已经介绍了JavaScript实际上还是一门面向于对象的语言,之所以显得比较“另类”是因为它的面向对象编程范式与其他基于类的主流编程语言如Java、C++等不同:基于原型,但是又因为一些公司政治原因,JavaScript在设计之初就被要求模仿Java,因此基于Brendan Eich又提出了new、this等关键字使之更加接近Java的语言特性,但是由于本质上面向对象编程范式的不同使得JavaScript并不具备Java的继承、多态等特性,因此JavaScript的开发社区出现了各种针对模仿基于类面向对象的封装,直到ES6正式提出class关键字,因此原型与类都是JavaScript对象的三大特性之一(另一个是扩展性,详见JavaScript中的对象(一):面向对象),本篇博客将详细为大家介绍JavaScript如何基于原型实现面向对象。

基于类的编程提倡使用一个关注分类和类之间关系的开发模型,在这类语言中总是先有类,再从类实例化一个对象,类与类之间又可能会形成继承、组合等关系,类又往往与语言的类型系统整合,形成一定编译时的能力。
与此相对,基于原型的编程更为提倡去关注一系列对象实例的行为,而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将他们分成类。基于原型的面向对象通过“复制”的方式创建新对象,一些语言的实现中还允许复制一个空对象,这实际上就是创建一个全新的对象。
原型系统的“复制操作”有两种实现思路:

  • 一个是并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用;
  • 另一个是切实地复制对象,从此两个对象再无关联。

而JavaScript是选择了第一种。

想要了解JavaScript中的基于原型面向对象的实现首先需要了解如下概念:

1、普通对象与函数对象
JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象(在JavaScript语言规范中明确指出函数是对象类型的一员),Object 、Function 是 JavaScript自带的函数对象。下面举例说明:

var o1 = {};
var o2 = new Object();
var o3 = new f1();
function f1(){};
var f2 = function(){};
var f3 = new Function('str');
 
typeof Object;//function
typeof Function;//function
typeof f1;//function
typeof f2;//function
typeof f3;//function
typeof o1;//object
typeof o2;//object
typeof o3;//object

在上面的例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。

2、构造函数
在JavaScript语言规范中对于构造函数的定义为:

构造函数是个用于创建对象的函数对象。每个构造函数都有一个 prototype 对象,用以实现原型式继承,作属性共享用。

示例:

function Person(name, age, job) {
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function() { alert(this.name) } 
}
var person1 = new Person('Zaxlct', 28, 'Software Engineer');
var person2 = new Person('Mick', 23, 'Doctor');

上面的例子中 person1 和 person2 都是 Person 的实例。这两个实例都有一个 constructor (构造函数)属性,该属性(是一个指针)指向 Person。 即:

console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true

注意构造函数也是函数,为了区分,构造函数的函数名首字母大写。(不是必须,但最好)

3、原型对象

JavaScript中定义的每个函数对象(Function.prototype除外,它是函数对象,但它很特殊,没有prototype属性)都有一个prototype 属性,这个属性指向函数的原型对象,换句话说,函数对象的prototype属性就是通过调用构造函数而创建的那个对象实例的原型对象,使用原型对象即可以让所有对象实例共享它所包含的属性和方法,可类比于基于类面向对象中父子类之间的继承,示例:

function Person(name) {
    this.name=name;
}
Person.prototype.eat=function () {
    console.log(this.name+"吃东西");
};
Person.prototype.sleep=function () {
    console.log(this.name+"睡觉");
}

// 所有通过调用构造函数Person()创建的对象共享原型对象上的属性和方法
var p1=new Person("小明");
p1.eat(); //小明吃东西
p1.sleep(); //小明睡觉
var p2=new Person("小红");
p2.eat(); //小红吃东西
p2.sleep(); //小红睡觉

// 原型对象的constructor属性指向构造函数
consloe.log(Person.prototype.constructor); // Preson(name) { this.name = name }

注意创建新函数时,其prototype属性指向的原型对象会自动获得一个constructor(构造函数)属性,该属性指向包含prototype的函数,也就是构造函数。
在前文中我们说到实例对象的constructor(构造函数)属性指向构造函数,那么原型对象的constructor属性为什么也指向构造函数呢?答案是原型对象(Person.prototype)本质上也是构造函数(Person)的一个实例。
原型对象其实就是普通对象,示例:

function Person(){};
console.log(Person.prototype) //Person{}
console.log(typeof Person.prototype) //Object
console.log(typeof Function.prototype) // Function
console.log(typeof Object.prototype) // Object
console.log(typeof Function.prototype.prototype) //undefined

Function.prototype 为什么是函数对象呢?我们前面说原型对象本质上也是构造函数的一个实例,因此我们不妨将原型对象看成构造函数在创建的时候同时创建了一个它的实例对象并赋值给它的 prototype,那么Function.prototype可以理解为:

var A = new Function ();
 Function.prototype = A;

通过 new Function( ) 产生的对象都是函数对象。因为 A 是函数对象,所以Function.prototype 是函数对象。

4、向上查找机制:__proto__
每个对象都拥有一个隐藏的属性[[prototype]],用于指向创建它的构造函数的原型对象。这个属性可以通过 Object.getPrototypeOf(obj) 或 obj.__proto__来访问,实际上,在ES6之前虽然大部分浏览器都支持通过__proto__属性来访问[[prototype]]属性,但并不是它并不是规范的一部分,直到ES6才被加入到规范中。
由于__proto__是任何对象都有的属性,因此 __proto__是用来实现向上查找的一个引用,对象可以通过 __proto__来寻找它构造函数的原型对象,__proto__ 将对象连接起来组成了原型链。注意Object.prototype 的 __proto__是 null,也即是原型链的终点。

示例:

function Cat(){}
var cat = new Cat();
console.log(cat.__proto__ === Cat.prototype); // true
console.log(Cat.prototype.__proto__ === Object.prototype) //true
console.log(Object.prototype.__proto__) //null

在这里插入图片描述
不过,要明确的真正重要的一点就是,__proto__属性形成的对象之间的连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

前面我们用于继承的原型链,它链接的是原型对象。而对象是通过构造函数生成的,也就是说,普通对象、原型对象、函数对象都将有它们的构造函数,这将为我们引出另一条链:
在这里插入图片描述
在 JavaScript 中,谁是谁的构造函数,是通过 constructor 来标识的。正常来讲,普通对象(如图中的 cat 和 { name: ‘Lin’ } 对象)是没有 constructor 属性的,它是从原型上继承而来;而图中粉红色的部分即是函数对象(如 Cat Animal Object 等),它们的原型对象是 Function.prototype,这没毛病。关键是,它们是函数对象,对象就有构造函数,那么函数的构造函数是啥呢?是 Function。那么问题又来了,Function 也是函数,它的构造函数是谁呢?是它自己:Function.constructor === Function。由此,Function 即是构造函数链的终结。

前面我们讲了两条链:

  • 原型链。它用来实现原型继承,最上层是 Object.prototype,终结于 null,没有循环
  • 构造函数链。它用来表明构造关系,最上层循环终结于 Function

把这两条链结合到一起我们得到下图:

在这里插入图片描述

  • 首先看构造函数链。所有的普通对象,constructor 都会指向它们的构造函数;而构造函数也是对象,它们最终会一级一级上溯到Function 这个构造函数。Function 的构造函数是它自己,也即此链的终结;
  • Function 的 prototype 是 Function.prototype,它是个普通的原型对象;
  • 其次看原型链。所有的普通对象,proto 都会指向其构造函数的原型对象 [Class].prototype;而所有原型对象,包括构造函数链的终点 Function.prototype,都会最终上溯到 Object.prototype,终结于 null。

也即是说,构造函数链的终点 Function,其原型又融入到了原型链中:Function.prototype -> Object.prototype -> null,最终抵达原型链的终点 null。至此这两条契合到了一起。

至此关于JavaScript中的原型与原型链就解释完毕了,下面梳理一下JavaScript中与原型相关的其他知识点:
1、Object.create()
Object.create()方法是ES6提供的创建一个新对象的另一种方式,使用现有的对象来提供新创建的对象的__proto__
语法:Object.create(proto[, propertiesObject])
参数:
proto:新创建对象的原型对象,可以为null,即用来创建空对象。
propertiesObject:可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数(关于Object.defineProperties()方法在我的博客JavaScript中的对象(一):面向对象中有详细介绍),注意创建非空对象的属性描述符默认都为false的。

注意:如果propertiesObject参数是 null 或非原始包装对象,则抛出一个 TypeError 异常。
使用Object.create()可以实现模仿基于类面向对象中的类式继承,示例:

// Shape - 父类(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父类的方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
  Shape.call(this); // call super constructor.
}

// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'

如果你希望能继承到多个对象,则可以使用混入的方式:

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do a thing
};

2、Object.setPrototypeOf()
Object.setPrototypeOf()方法的作用与直接设置__proto__相同,用来设置一个对象的 prototype 对象,返回参数对象本身,它是 ES6 正式推荐的设置原型对象的方法。
语法:Object.setPrototypeOf(object, prototype)
示例:

var proto = {
    y: 20,
    z: 40
};
var obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
console.log(obj); 

在这里插入图片描述
3、Object.getPrototypeOf()
Object.getPrototypeOf()用于读取一个对象的原型对象;
语法:Object.getPrototypeOf(obj);
示例:

Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true

4、new
在前面讲解到构造器函数的时候其实我们已经在示例中使用了new操作符,new运算虽然在JavaScript中主要是针对于构造器对象而不像其他基于类的编程语言一样针对于类,但是new仍然是JavaScript面向对象的重要一部分,new运算接受一个构造器和一组调用参数,实际上做了几件事:

  • 以构造器的prototype属性为原型创建新对象;
  • 将this和调用参数传给构造器,执行;
  • 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。

new这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的prototype属性上添加属性,下面示例展示了用构造器模拟类的两种方法:

function c1() {
	this.p1 = 1;
	this.p2 = function() {
		console.log(this.p1)
	}
}
var o1 = new c1;
o1.p2();

function c2() {}
c2.prototype.p1 = 1;
c2.prototype.p2 = function() {
	console.log(this.p1);
}
var o2 = new c2;
o2.p2();

在没有Object.create()、Object.setPrototypeOf()的早期版本中,new运算是唯一一个可以指定对象[[prototype]]属性的方法(__proto__多数环境不支持),所以当时已经有人试图用它来代替后来的Object.create(),我们甚至可以用它来实现一个不完整的polyfill:

Object.create = function(prototype) {
	var cls = function(){}
	cls.prototype = prototype;
	return new cls;
}

这段代码创建了一个空函数作为类,并把传入的原型挂在了它的prototype,最后创建了一个它的实例,根据new的行为,这将产生一个已传入的第一个参数为原型的对象。但是这个函数无法做到与原生的Object.create()一致,一个是不支持第二个参数,另一个是不支持null作为原型。

参考资料:
极客时间《重学前端》专栏
JavaScript 原型精髓 #一篇就够系列
一张图理解JS的原型(prototype、proto、constructor的三角关系)

猜你喜欢

转载自blog.csdn.net/weixin_44196299/article/details/100145637