prototype原型与继承
关于prototype原型、继承、函数构造
关于prototype,我们首先需要理解的就是JS中关于面对对象编程的部分,第一部分就是理解对象,对象字面量方法是当今创建对象的首选方法方法,而在es6中更是有关于对象创建的拓展。
//对象字面量方法
let person = {
name: 'woo',
age: 20,
job: 'student',
sayName: function() {
console.log(this.name);
}
}
如上所看到的就是对象字面量方法,而在es6中对于函数的申明,更是有升级的办法(箭头函数),这个部分就先暂时不说明,箭头函数与普通函数的区别,大家可以上网搜索。在这篇博客中我使用的是普通函数(function name() {})的形式
每个对象中的数据属性都含有4个特性:[[Configurable]],[[Enumerable]],[[Writable]],[[Value]]
- [[Configurable]]:表示能否通过delete删除属性,当为默认属性为true,可以删除。
- [[Enumerable]]:表示能否通过for-in循环返回属性,当为默认属性为true,可以循环。
- [[Writable]]:表示能否修改这个属性的值,默认为true,可以修改。
- [[Value]]:包含这个属性的数据值。
要修改这些特性可以通过Object.defineProperty()方法。
1.访问器属性
访问器属性不包含数据值,一般包含一对getter和setter。
访问器属性含有4个特性:[[Configurable]],[[Enumerable]],[[Get]],[[Set]]
- [[Configurable]]:表示能否通过delete删除属性,当为默认属性为true,可以删除。
- [[Enumerable]]:表示能否通过for-in循环返回属性,当为默认属性为true,可以循环。
- [[Get]]:在读取属性时调用的函数。
- [[Set]]:在写入属性的时候调用的函数。
要修改这些特性可以通过Object.defineProperty()方法。
let book = {
_year: 2001,
edition: 1
};
Object.defineProperty(book, 'year', {
get: function () {
return this._year + this.edition
},
set: function (newValue) {
this._year = newValue + this.edition;
++this.edition;
}
})
book.year; //2002
book.year = 2005;
book.edition; //2
ES5中定义了一个object.defineProperties(对象,对象的属性对象)方法,对多个属性的特性进行修改。通过Object.getOwnPropertyDescriptor(对象,属性)方法可以取得给定属性的描述特性
在这里可以抛出一个问题,是不是每执行一个语句都会返回一个值
这个问题大家可以去了解一下,因为看上述的代码,如果你在谷歌浏览器的开发者模式中打开,然后执行上述代码,你会发现会返回一个2那么这么2是从哪里来的,大家可以了解一下。
1.2工厂模式
一开始是考虑到ES中无法创建类,开发人员就发明了这种模式。
//工厂模式
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.jog = job;
return o;
}
当然随着js的发展逐渐由了 Object.create(…) 替代了其的作用。
2.构造函数
构造函数可以用创建特定类型的对象,也可以穿件自定义的构造函数,从而定义自定义对象的属性和方法。下面代码就是一个构造函数的构造过程。构造函数始终应该以一个大写字母开头,而非构造函数应该以一个小写字母开头。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.jog = job;
this.sayName = function () {
console.log(this.name);
}
}
var person1 = new Person('woo', '18', 'student');
在这个例子里面Person取代了工厂中的createPerson()函数,而且使用Person加new创建了一个实例。当然,这样子也有几个不同之处:
- 没有显示的创建对象
- 直接将方法和属性赋予this对象
- 没有return对象
在上述的例子中,所创建的所有对象既是Obbject的实例,也是Person的实例。创建自定义构造函数意味可以将它的实例表示为一种特定的类型。
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
如果将构造函数当做普通的函数使用,其在函数内的属性和方法将会被添加到window对象中,在调用完函数之后,可以通过window对象访问到其中发方法和属性。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.jog = job;
this.sayName = function () {
console.log(this.name);
}
}
Person('woo', '18', 'student');
window.sayNanme(); //'woo'
== 构造函数 == 也有其确定,那就是每次创建实例的时候其中的方法都要重新创建一遍。更明白一些就是以这种方式创建函数,会导致不同的作用于链和标识符解析就算死不同实例上的同名函数也是不相等的。当然也有好和不好,这样子重新创建能避免方法修改而造成的相互影响,也有不好的地方那就是,每创建一个属性且不许对方法做出修改的时候,就相当于创建了更多的内存去存储相同的东西。当然我们还要注意一个点,那就是new 会劫持所有普通函数并用构造对象的形式来调用它。
function NothingSpecial() {
console.log( "Don't mind me!" );
}
var a = new NothingSpecial(); // "Don't mind me!"
a; // {}
NothingSpecial 只是一个普通的函数,但是使用 new 调用时,它就会构造一个对象并赋值 给 a,这看起来像是 new 的一个副作用(无论如何都会构造一个对象)。这个调用是一个构 造函数调用,但是 NothingSpecial 本身并不是一个构造函数。
3.原型模式
而要解决其中的多方法冗余问题,我们可以考虑使用原型进行解决。因为每个函数都有一个prototype属性,这个属性是一个指针。在我的理解里面,这个指针的意义就是它会指向另外一个对象,从而达到共享这个对象的属性好方法的作用。要记住prototype是不可访问的但是可以通过isPrototypeof(函数的属性名字)方法或者Object.getPrototypeof(函数实例名例如下面的person1)确认对象之间的这种关系。如下面的例子:
function Person() {
}
Person.prototype.name = 'woo';
Person.prototype.age = 18;
Person.prototype.job = 'student';
Person.prototype.sayName = function () {
console.log(this.name);
}
let person1 = new Person();
person1.sayName(); //'woo'
let person2 = new Person();
person2.sayName(); //'woo'
console.log(person1.sayName === person2.sayName); // true
console.log(Person.prototype.isPrototypeof(person1)); //true
前面的Person构造函数、Person原型属性以及Person现有的两个实例之间的关系如下图,其实从中可以看出,其实创建的实例同时指向的是Person的prototype而不是Person这个函数。所以实例其实和构造函数没有实际关系。
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性(这个属性可以是通过prototype获得而本身并没有,在后续中会对constructor的由来)。当调用构造函数创建一个实例化后,该实例的内部就创建了一个指针指向原型对象,虽然在脚本中我们标准的方法访问[[prototype]]对象,但是我门可以通过_proto_属性进行访问,但是这个属性相当于通过原型链进行查找的,而这个属性其实是个getter和setter。具体的实现是这样子的。
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// ES6 中的 setPrototypeOf(..)
Object.setPrototypeOf( this, o );
return o;
}
} );
constructor属性
其实constructor属性在我看来是非常难理解的一个属性,假设有个函数a.constructor === Foo为真是否意味着a确实有一个指向Foo的.constructor函数。但是实际上 .constructor引用同样是被委托给了Foo.prototype,而Foo.prototype.constructor 默认指向 Foo。a.constructor 只是通过默认的 [[Prototype]] 委托指向 Foo,这和“构造”毫无关系。
Foo.prototype 的 .constructor 属性只是 Foo 函数在声明时的默认属性。如果 你创建了一个新对象并替换了函数默认的 .prototype 对象引用,那么新对象并不会自动获 得 .constructor 属性。这个就好像是中断效果一样。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
在我以前会认为’constructor’是“由什么构造的意思”,但是那样子上述的代码中constructor应该指向Foo而不是object、
最够归根结底是a1并没有constructor属性,所以它会通过prototype原型链上进行寻找(即Foo.prototype)。但是这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这 个属性!),所以它会继续委托,这次会委托给委托链顶端的 Object.prototype。这个对象 有 .constructor 属性,指向内置Object(…) 函数。
你可以给Foo.prototype属性手动添加一个.constructor属性,但是那样子将变得可以枚举,除非手动设置其特性。
属性的重写
虽然我们可以通过对象实例访问原型的值,如果我们重写其中的值时不可能的,如果我们对其进行赋值,那么我们可能只是对实例进行同名属性的赋值从而屏蔽了原型的值。以 myObject.foo = “bar” 为例子对这个属性的重写进行说明。
在于原型链上层时 myObject.foo = “bar” 会出现的三种情况。
- 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
- 如果在 [[Prototype]] 链上层存在 foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
- 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter,那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。
当然对于第一种方法却可能有一种隐式屏蔽要注意的,如下面例子
var anotherObject = { a:2 };
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
同时使用in操作符和hasOwnProperty()方法可以知道该属性到底实在实例中还是原型中。
function hasOwn(object, name) {
return !object.hasOwnProperty(name) && (name in object)
}
在使用for-in循环中可以遍历实例和原型对象中的属性,但是在IE8中因为实例中自定义了原型对象中的属性,所以因为屏蔽而不能使用。
更简单的使用原型
使用上面的例子:
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
这个例子能更简单的使用原型,但是这个例子确会是constructor不再指向Foo,所以在创建的时候我们可以考虑添加这个属性再利用defineProperty将其变成不可改写。
现在就说说原型不好的地方,那就是当通过prototype修改方法的时候,所有的实例的该方法将会得到修改。这有好处有坏处,那就是我们可能在以前定义了一个prototype方法,但是别人在不知道的情况下,重新定义,从而造成报错。而对原生引用类型(object,array,string等)如果在其中定义更是要防止,在后续的语言标准修订中,该方法不会被添加入标准中。
原生函数省略了为构造函数初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,归根结底就是原生模式的prototype属性多指向的对象很多时候是共享的。
当然还有就是我们给prototype添加新的方法的时候,实例也能马上得到共享。
尽管可以随时为原型添加属性和方法,但是如果是重写整个原型对象情况会不一样。因为在调用构造函数时会为实例添加一个指向最初原型指针,而吧原型修改为另外一个对象就等于切断构造函数与最初原型之间的联系。这样子实例中的指针只指向原型而不指向构造函数.
function Person() {
}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nichoi",
age: 29,
job: '1212',
sayName: function () {
return this.name;
}
};
friend.sayName(); //会报错friend.sayName is not a function
如图所示就相当于已经指向了不同的对象。
为了改善原型问题和构造函数问题,我们可以选择
- 组合使用构造函数和原型函数:构造函数定义属性,原型构造方法。
- 动态原型模式:在每次构造函数时,判断是否有该方法,如果没有就添加。
- 寄生构造函数:会类似工厂模式返回对象。
- 稳妥构造函数:禁止使用this和new。
4.继承
其实和继承有关的就是原型链,在上述中,我们一直都很疑惑为什么我们继承prototype就能得到原型中的方法,继承其中的基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。这大概就是原型的基本概念。
实现原型链等我一种基本模式如下:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subpro = false;
}
//继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubpro = function () {
return this.subpro;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //true
这个就是上述代码的继承原型链。在这个代码中subType的prototype使用了SuperType的实例,于是有了作为SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向SuperType的原型。当然不要漏了个最最重要的也是默认的原型Object,也是最终指向的类型也可以说在JS中万物起源于object对象。
原型链的问题
其实原型链的问题就是实例中原型函数的共享方法问题,在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是原先的实例属性就变成了现在的原型属性了。
function SuperType() {
this.color = ['red'];
}
function sub() {
}
sub.prototype = new SuperType();
let inst1 = new sub();
inst1.color.push('black');
console.log(inst1.color); //'red,black'
let inst2 = new sub();
console.log(inst2.color); //'red,black'
第二个问题就是在创建不能像超类型的构造函数中传递参数。
继承的各种方式
- 原型式继承
- 组合继承
- 寄生式继承
- 继承组合式继承
5.小结
其实面对js
各种的继承与原型,我们可以通过 Object.create(); 这个方法创建对象赋予prototype属性。Object.create(…) 会创建一个新对象并把它关联到我们指定的对象,这样 我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使 用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。
var foo = {
something: function() {
console.log( "Tell me something good..." );
}
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...