js原理-js原型prototype

2 浏览发布于 作者 zouyang留下评论分享按钮

一、本文缘由
js的各类主流书籍大都涉及到了原型的讲解,但是每本书的侧重不一,部分书还有内容上的出入,有的讲了这部分,有的讲了另一部分。
本文是我原型笔记汇总提炼而出(暂不涉及原型与模式的配合使用的用例知识)。

(本文主要是知识点梳理,不是教程)


二、概念依次解读

1、原型对象:prototype 源自法语,软件界的标准翻译为“原型”,代表事物的初始形态,也含有模型和样板的意思。—《悟透js》
每一个函数(包括构造函数及其它普通函数)都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,就是原型对象。—《js高级程序设计》
既然原型是个对象,那么它就可以拥有属性和方法。

2、js中的”类”:js是一门解释型、动态、无类型、基于原型继承的语言,它没有像其它静态语言 “类 class” 的概念,它是通过构造函数来实现一个类的概念的(类似的怪异还有比如数组,js内没有数组数据结构,而是提供的类数组array-like特性的对象— 题外话):

//Java 中通过类去实例化一个对象 
public class Person{} //实例化 Person 类,
//生成Jack对象: 
Person Jack = new Person(); 

//js 中通过构造函数去实例化一个对象
function Person(){} //构造函数 首字母需大写 
//实例化 Person 构造函数,生成Jack对象: 
var Jack = new Person();

在 js 中,函数就是一个对象。

3、原型对象 prototype 的访问及共享:
1)访问原型对象:通过 构造函数下的 prototype 属性访问获取

function Person(){} 
console.log( Person.prototype );  
//{constructor: ƒ Person()} 因为目前我们没有给原型对象内自定义添加属性和方法,所以原型对象目前只有默认包含的一个属性,即 constructor 属性(后面第5点会专门说这个 constructor 属性)(有的浏览器还会提供一个属性 __proto__ 属性,后面第4点会说)

补充:普通函数也是可以获取到原型对象,不单单构造函数(构造函数与普通函数并无二致,其实都是函数),只是我们一般都用构造函数来实例化对象:(这个属于构造函数的知识,不多涉及)

//普通函数的原型对象
function box(){} 
console.log( box.prototype ); //{constructor: ƒ box()}


2)共享原型对象:
当你通过构造函数创建一个对象时,就会同时将 构造函数 的 prototype 原型对象指派给新创建的对象,称为该新创建对象的内置的原型对象。
这样就把构造函数的原型对象 prototype 共享 给了创建的 实例对象,该实例对象的原型对象就等同于 构造函数的原型对象,实例对象就可以使用原型上的属性方法,我们在构造函数的原型上添加各种属性和方法,该构造函数所创建的对象上也能调用到那些属性方法。
比如让所有实例都具备 eat 吃饭的能力(方法)
(题外话:对象内置的原型对象应该是对外不可见的,尽管有些浏览器提供了 __proto__ 这个属性让我们可以访问这个内置原型对象,但不建议这么做,下一个 知识点4 会讲 __proto__

function Person(){} 
Person.prototype.eat = function(){ console.log("我会吃饭"); } //在构造函数 Person 的原型上添加 方法 eat 
var Jack = new Person(); 
var Lee = new Person(); 
Jack.eat();	//我会吃饭
Lee.eat();	//我会吃饭



4、在实例上访问 构造函数的原型对象,需通过 proto指针:(也包括其它字面量形式声明的对象等等对象)
这些实例化后的对象的原型对象 prototype 是对外不可见的,要访问它们的 原型对象 prototype 得借助一个该对象下的隐藏属性 __proto__(称为 隐藏属性,是因为只有部分浏览器才提供了该属性接口,比如 Firefox、Safari、Chrome)
__proto__ 是指向 实例化该对象的那个构造函数(构造器)的原型对象 的指针,通过它就可以访问到之前无法访问的构造函数(构造器)上的原型对象 prototype 了。__proto__ 默认会指向构造器的原型对象。—《js高级程序设计》《悟透js》、《js设计模式与开发实践》

__proto__ 的由来:实例化该对象的那个 构造函数的原型对象 的指针,在 ECMA-262 第五版中管这个 指针 叫:[[ Prototype ]]。脚本中没有标准方式访问 [[ Prototype ]],所以才有了浏览器提供的 __proto__ 来提供访问。

举例:

function Person(){} 
var Jack = new Person(); 
console.log( Person.prototype ); //{constructor: ƒ Person()} 
console.log( Jack.prototype ); //undefined 实例化后的对象是无法直接访问原型prototype的 
console.log( Jack.__proto__ ); //{constructor: ƒ Person()} 通过指针便可访问到

我们在控制台展开 Person.prototype 打印的 {constructor: ƒ Person()} ,会看到:

可以看出 Person 构造函数的原型对象内还有一个 __proto__ 指针指向 Object。(这个就是原型链的构成,后面会专门说,这里就知道有这么个东西就OK)

//在上一个例子上继续,我们给 Person 函数的原型添加一个属性 num:
Person.prototype.num = 123;

console.log( Person.prototype ); //{num: 123, constructor: ƒ Person()} 
console.log( Jack.prototype ); //undefined 实例化后的对象是无法直接访问原型prototype的,这里打印的 Jack 下的是 prototype 属性,因为我们没有定义过该属性,所以为 undefined
console.log( Jack.__proto__ ); //{num: 123, constructor: ƒ Person()} 

补充,ES5 提供了一个 Object.getPrototypeOf() 方法,该方法返回指定对象的原型(内部[[Prototype]]属性的值):

function User() {}
var u = new User();
console.log( Object.getPrototypeOf(u) === User.prototype );  //true

//ES5 之前,我们就只有用这个 __proto__ 指针来检测:
console.log( u.__proto__=== User.prototype );  //true



5、constructor 构造函数的原型对象内的默认属性
每一个 js 函数都自动拥有一个 prototype 属性,这个属性就是 原型对象,它包含一个不可枚举的 constructor 属性。constructor 属性的值是一个函数对象。 —《js权威指南》
constructor 属性包含的函数对象是 该 prototype 原型对象所属 函数 的指针(一个函数对象)。通过 constructor 属性,我们就可以了解到这个原型对象是和哪个函数相关联的。

var F = function(){};
var a = new F();
console.log(a.constructor === F);  //true



6、以上我们实例化对象都是通过 构造函数,其它字面量形式声明的等等对象,也都是具有原型对象的。JS内的对象都是从 Object 原型链上继承来的。

var box= {};
console.log( box.prototype ); //undefined
console.log( box.__proto__ ); //{constructor: ƒ, __defineGetter__: ƒ, …} 



上面6点,总结一下:

  • 每个构造函数,都有一个原型对象;
  • 该原型对象,会包含一个默认属性 constructor,用来表明 该原型对象所属的函数;
  • 实例化出来的 实例对象(通过函数),包含一个 指向 实例化该对象的那个(构造)函数的原型 的指针:__proto__



三、原型链

假如,我们让 类型A 的原型对象 等于 另一个 类型B 的实例X,会发生什么呢?

此时,类型A 的原型对象 将指向 类型B的实例X(对象),类型A 的原型对象 就将具有 实例X(对象) 的属性和方法,同时因为 实例X 有一个指向 类型B 的原型,所以 类型A 的也会具有 类型B 的原型对象上的属性和方法。
此时,类型A 的实例在读取属性和方法调用时,会查找 类型A里有没有,如果没有就去 类型A 的 prototype 原型对象上找,就会找到 实例X ,如果还是没有 则会去 实例X 的原型对象上去找,就是 类型B 的原型对象。
假如 类型B 的原型对象又是指向另一个其它类型的实例,上述关系以此类推下去,如此层层递进,就构成了实例与原型的链条,这就是原型链。
因为 类型A 的原型对象被我们修改了,所以原本 constructor 应该是 类型A,现在就被修改为了 实例X 了。

示例:

//来自《js高程》
function SuperType() {
    this.property = 1;
}
SuperType.prototype.getSuperValue = function () {
    return this.property;
}
function SubType() {
    this.subproperty = 2;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
    return this.subproperty;
}
var oSubType = new SubType();
console.log(oSubType.property);   //1
console.log(oSubType.__proto__.property);   //1
console.log(oSubType.__proto__.constructor);   //ƒ SuperType(){this.property = 1;}
console.log(oSubType.subproperty);   //2
console.log(oSubType.getSuperValue());   //1
console.log(oSubType.getSubValue());   //2

原型链,也可以理解为:当从一个对象那里读取属性和方法调用时,如果该对象自身不存在这样的属性和方法,就会去自己的 prototype 对象里查找;如果 prototype 里没有,又会去 prototype 自己关联的前辈 prototype 那里查找,直到找到或追溯过程结束 为止(这样的追溯机制,就是 prototype链,也叫 原型链)。


在原型链的末端,就是 Object 构造函数的 prototype 属性指向的那一个原型对象。这个老祖宗实现了诸如 toString等所有对象天生就该具备的方法。所以,所有函数的默认原型都是 Object 的实例,默认原型内部会有一个指针,指向 Object.prototype,我们的 toString 正式定义在 Object.prototype 上的,所以我们的函数都天生拥有了 toString 等等方法。


上一个例子就是一句话总结:SubType 继承了 SuperType,而 SuperType 又继承了 Object。 当调用 oSubType.toString() 时,是调用的 Object.prototype 上的 toString 方法。

通过原型链,我们就知道了,js的对象继承是通过 原型继承 来实现的了。

通过原型链继承,需要注意2个问题:

  • 继承的原型里包含引用类型的值,如果对该值进行了修改,会影响到所有与该原型有关系的对象。
  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
function SuperType() {
    this.colors = ["red", "blue"]; //引用类型
    this.age = 100;
}
function SubType() {}

//继承了 SuperType
SubType.prototype = new SuperType();

var oSubType1 = new SubType();
console.log(oSubType1.colors);   //["red", "blue"]
console.log(oSubType1.age);   //100

oSubType1.colors.push("yellow"); //修改 引用类型
oSubType1.age = 101; //修改基本类型
console.log(oSubType1.colors);   //["red", "blue", "yellow"]
console.log(oSubType1.age);   //101

var oSubType2 = new SubType();
console.log(oSubType2.colors);   //["red", "blue", "yellow"]   也受影响了
console.log(oSubType2.age);   //100



四、拾遗-零散知识

对象可以覆盖原型对象的属性和方法;

我们还可以动态的给原型对象添加新的属性和方法,从而动态的扩展基类的功能。这在静态语言中是很难想象的。—《悟透js》

通过字面量(直接量)创建的对象,都具有同一个原型对象:Object.prototype —《js权威指南》

Object.prototype 是为数不多的 没有原型的 对象。并且该对象还是只读的。
Object.prototype = 0; //失败

ES5定义的:Object.create(),它可以传入 一个 对象 作为原型,也可以传入一个 null 来创建一个没有原型的新对象。—《js权威指南》

var box = Object.create({x:1,y:2}); // box 继承了属性 x和y
var box2 = Object.create(null); //没有原型的对象,不继承任何属性和方法
var box3 = Object.create(Object.prototype); //创建一个普通的空对象,等同于{}、new Object()

Object.create是为了规范原型式继承而来的:《js高程》 page169

Object.create 在不支持时的兼容做法,在 ES5 之前的环境中(比如旧 IE) 如果要支持这个功能的话就需要使用一段简单的 polyfill 代码, 它部分实现了 Object.create(..) 的功能:

if (!Object.create) {
    Object.create = function (o) {
        function F() { }
        F.prototype = o;
        return new F();
    };
}

对象调用属性或方法时,有就近原则,如果既有原型name属性,也有实例name属性,则alert(box.name)调用的是实例属性name,就近原则。

每一个函数相当于可以有 实例属性  与  原型属性之分。  判断name是否为实例属性可以用: hasOwnProperty()  例如:alert( box. hasOwnProperty(‘name’) );

in 可以判断name是否为 实例属性或原型属性,例如:   alert( ‘name’ in box );

for in 循环会枚举原型链上的所有属性,过滤这些属性的方式是在循环中配合使用 hasOwnProperty 函数

for (var val in obj) {
    if (obj.hasOwnProperty(val)) { //确保只处理实例自身属性
        //……
    }
}

你不知道的js 上册 P155 setPrototypeOf

在需要的时候,可以使用 null 来防止原型污染:
function F(){};
F.prototype = null;
ES5的方式:var a = Object.create( null );



五、拾遗-原型赋值
因为篇幅较多,我直接引用原文,就不再整理了:《你不知道的js》上册照片

注意上图红框处,这样的原型继承 是会有副作用的,下面是具体原因以及ES6的 setPrototypeOf讲解:

感兴趣可以看书上原文。另外,悟透js page47原型真谛的原型模拟类使用的语法甘露值得了解。


(完)

想要打赏,请点击这里

发表评论

电子邮件地址不会被公开。 必填项已用*标注