文章同步到githubhtml
建立对象的几种方式git
原型模式github
继承segmentfault
我的扩展补充浏览器
直接上《JavaScript高级教程》的截图app
补充:
js中说一切都是对象,是不彻底的,在js中6种数据类型(Undefined,Null,Number,Boolean,String,Object)中,前五种是基本数据类型,是原始值类型,这些值是在底层实现的,他们不是object,因此没有原型,没有构造函数,因此并非像建立对象那样经过构造函数建立的实例。关于对象属性类型的介绍就不介绍了,能够看我上一篇文章Object.defineProperty()和defineProperties()ecmascript
var obj = new Object();
var obj = {};
若是使用构造函数和字面量建立不少对象,每一个对象自己又有不少相同的属性和方法的话,就会产生大量重复代码,每一个对象添加属性都须要从新写一次。如两个对象都须要添加name、age属性及showName方法:函数
var p1 = new Object(); p1.name = '张三' p1.age = '16', p1.showName = function() { return this.name } var p2 = new Object(); p2.name = '李四' p2.age = '18', p2.showName = function() { return this.name }
为了解决这个问题,人们采用了工厂模式,抽象了建立对象的过程,采用函数封装以特定接口(相同的属性和方法)建立对象的过程。this
function createPerson(name, age) { var obj = new Object(); obj.name = name; obj.age = age; obj.showName = function () { return this.name; }; return obj; } var p1 = createPerson('张三', 16); var p2 = createPerson('李四', 18);
虽然工厂模式解决了建立多个对象的多个相同属性问题,却没法断定对象的具体类型,由于都是Object,没法识别是Array、或是Function等类型,这个时候构造函数模式出现了。spa
js中提供了像Object,Array,Function等这样的原生的构造函数,同时也能够建立自定义的构造函数,构造函数是一个函数,用来建立并初始化新建立的对象。将工厂模式的例子用构造函数能够重写为:
function Person(name, age) { this.name = name; this.age = age; this.showName = function() { console.log(this.name); } } var p1 = new Person('张三', '16'); var p2 = new Person('李四', '18');
用Person代替了工厂模式的createPerson函数,并且函数名首字母P大写,这是由于按照惯例,构造函数首字母应该大写,而做为非构造函数的函数首字母小写。另外能够注意到构造函数内部的特色:
另外,还使用了new操做, 要建立一个实例,必须使用new操做符,使用new操做符调用构造函数,在调用构造函数的时候经历了以下几个阶段:
用伪代码来讲明上述new Person()的过程以下:
// 使用new操做符时,会激活函数自己的内部属性[[Construct]],负责分配内存 Person.[[Construct]](initialParameters): // 使用原生构造函数建立实例 var Obj = new NativeObject() //NativeObject为原生构造函数,如Object、Array、Function等 // 给建立的实例添加[[Class]]内部属性,字符串对象的一种表示, 如[Object Array] // Object.prototype.toString.call(obj)返回值指向的就是[[Class]]这个内部属性 Obj.[[Class]] = Object/Array/Function; // 给建立的实例添加[[Prototype]]内部属性,指向构造函数的prototype O.[[Prototype]] = Person.prototype; // 调用构造函数内部属性[Call],将Person执行上下文中this设置为内部建立的对象Obj, this = Obj; result = Person.[[Call]](initialParameters); // result是若是构造函数内部若是存在返回值的话,调用[[call]]时做为返回值,通常为Object类型 // 调用Person.[[call]]时,执行Person中的代码,给this对象添加属性和方法 this.name = name; this.age = age; this.showName = function() { console.log(this.name); }; //若是Person.[[call]]的返回值result为Object类型 return result // 不然 return Obj;
补充,贴出ECMAScript 5.1版本标准中[[Construct]]的规范,我本人对[[Call]]的返回值问题理解的也很差,但愿哪位大神能够指点指点。
构造函数虽然解决了实例多个同名属性重复添加的问题,可是也存在每一个实例的方法都须要从新建立一遍,由于每一个方法都是Function的不一样实例,看下面这段代码就明白了:
function Person(name, age) { this.name = name; this.age = age; this.showName = new Function("console.log(this.name);"); } var p1 = new Person('张三', '16'); var p2 = new Person('李四', '18'); console.log(p1.showName === p2.showName); //false
这个问题能够用如下办法来解决,把showName变成全局函数
function Person(name, age) { this.name = name; this.age = age; this.showName = showName; } function showName() { console.log(this.name) }
可是这样若是对象须要添加不少方法就会产生不少全局函数,这些问题能够经过原型模式来解决
当每个函数建立时,都会给函数设置一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象包含全部实例共享的属性和方法,在默认状况下,都会为prototype对象添加一个constructor属性,指向该函数。
Person.prototype.constructor = Person;
原型模式就是没必要在构造函数中定义实例的属性和方法,而是将属性和方法都添加到原型对象中。建立自定义构造函数,其原型对象只会默认取得constructor属性,其余的属性和方法都是从Object继承来的。当使用构造函数建立一个实例以后,会给实例添加内部属性[[prototype]],这个属性是一个指针,指向构造函数的prototype(原型)对象,因为是内部属性,没法经过脚本获取,可是在一些Chrome、Firefox、Safari等浏览器中在每一个对象身上支持一个__proto__属性,指向的就是构造函数的原型对象。另外能够经过isProtoTypeOf()来判断建立的实例是否有指向某构造函数的指针,若是存在,返回true,若是不存在,返回false。
function Person() { } Person.prototype.name = '张三'; Person.prototype.friends = ['张三', '李四']; Person.prototype.showName = function() { console.log(this.name); } var p1 = new Person(); var p2 = new Person() console.log(p1.__proto__ === Person.prototype) // true console.log(Person.prototype.isPrototypeOf(p1)) // true
在ECMA5中增长了一个方法Object.getPrototypeOf(params),返回值就是建立对象的原型对象
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true console.log(Object.getPrototypeOf(p1).name); //张三
原型模式虽然解决了方法共享的问题,可是对于实例共享来讲是个比较大的问题,由于每一个实例都须要有描述本身自己特性的专有属性,仍是上面的代码:
console.log(p1.name) // '张三' console.log(p2.name) // '张三'
另外对于属性是引用类型的值来讲缺点就更明显了,若是执行下面这段代码:
p1.friends.push('王五'); console.log(p1.priends); //['张三', '李四', '王五'] console.log(p2.priends); //['张三', '李四', '王五']
为了解决原型模式的问题,人们采用了原型和构造组合模式,使用构造函数定义实例,使用原型模式共享方法。
直接上代码:
function Person(name, age) { this.name = name; this.age = age; this.friends = ['张三', '李四']; // this.friends = new Array('张三', '李四') } Person.prototype.showName = function() { console.log(this.name); }; var p1 = new Person('John'); var p2 = new Person('Alice'); p1.friends.push('王五'); console.log(p1.friends); // ['张三', '李四', '王五']; console.log(p2.friends); // ['张三', '李四']; // 由于这时候每一个实例建立的时候的friends属性的指针地址不一样,因此操做p1的friends属性并不会对p2的friends属性有影响 console.log(p1.showName === p2.showName) // true 都指向了Person.prototype中的showName
这种构造函数模式和原型模式组合使用,基本上能够说是js中面向对象开发的一种默认模式,介绍了以上这几种经常使用建立对象的方式, 还有其余不经常使用的模式就不介绍了,接下来想说的是js中比较重要的继承。
ECMA中继承的主要方法就是经过原型链,主要是一个原型对象等于另外一个类型的实例,因为实例内部含有一个指向构造函数的指针,这时候至关于重写了该原型对象,此时该原型对象就包含了一个指向另外一个原型的指针,假如另外一个原型又是另外一个类型的实例,这样就造成了原型链的概念,原型链最底层为Object.prototype.__proto__,为null。
js中实例属性的查找,是按照原型链进行查找,先找实例自己有没有这个属性,若是没有就去查找查找实例的原型对象,也就是[[prototype]]属性指向的原型对象,一直查到Object.prototype,若是仍是没有该属性,返回undefined。全部函数的默认原型都是Object实例。
function Parent() { this.surname = '张'; this.name = '张三'; this.like = ['apple', 'banana']; } var par = new Parent() function Child() { this.name = '张小三'; } Parent.prototype.showSurname = function() { return this.surname } // 继承实现 Child.prototype = new Parent(); var chi = new Child(); console.log(chi.showSurname()) // 张
以上代码证实,此时Child实例已经能够访问到showSurname方法,这就是经过原型链继承Parent原型方法,剖析一下其过程:
Child.prototype = new Parent();
至关于重写了Child.prototype,指向了父实例par,同时也包含了父实例的[[prototype]]属性,此时
console.log(Child.prototype.__proto__ === par.__proto__); // true console.log(Child.prototype.__proto__ === Parent.prototype); // true
执行chi.showSurname()时,根据属性查找机制:
console.log(chi.showSurname()) // 张
补充:
全部函数默认继承Object:
function Person() { } console.log(Person.prototype.__proto__ === Object.prototype); // true
只经过原型来实现继承,还存在必定问题,因此js中通常经过借用构造函数和原型组合的方式来实现继承,也称经典继承,仍是继承那段代码,再贴过来把,方便阅读
function Parent() { this.surname = '张'; this.name = '张三'; this.like = ['apple', 'banana']; } var par = new Parent() function Child() { this.name = '张小三'; } Parent.prototype.showSurname = function() { return this.surname } // 继承实现 Child.prototype = new Parent(); var chi1 = new Child(); var chi2 = new Child(); console.log(chi.showSurname()) // 张 // 主要看继承的属性 console.log(chi.like) // ['apple', 'banana'] 这是由于Child.prototype指向父实例,当查找实例chi自己没有like属性,就去查找chi的原型对象Child.prototype,因此找到了
那么还存在什么问题呢,主要就是涉及到引用类型的属性时,引用类型数据的原始属性会被实例所共享,而实例自己的属性应该有实例本身的特性,仍是以上代码
chi.like.push('orange'); console.log(chi1.like); // ['apple', 'banana', 'orange'] console.log(chi2.like); // ['apple', 'banana', 'orange']
因此构造函数和原型组合的经典继承出现了,也是本篇最重要的内容:
1.属性继承
在子构造函数内,使用apply()或call()方法调用父构造函数,并传递子构造函数的this
2.方法继承
使用上文提到的原型链继承,继承父构造器的方法
上代码:
function Parent(name) { this.name = name; this.like = ['apple', 'banana']; } Parent.prototype.showName = function() { console.log(this.name); }; function Child(name, age) { // 继承属性 Parent.call(this, name); // 添加本身的属性 this.age = age; } Child.prototype = new Parent(); // 子构造函数添加本身的方法 Child.prototype.showAge = function() { console.log(this.age); }; var chi1 = new Child('张三', 16); var chi2 = new Child('李四', 18); chi1.showName(); //张三 chi1.showAge(); //16 chi1.like.push('orange'); console.log(chi1.like); // ['apple', 'banana', 'orange'] console.log(chi2.like); // ['apple', 'banana']
在子构造函数Child中是用call()调用Parent(),在new Child()建立实例的时候,执行Parent中的代码,而此时的this已经被call()指向Child中的this,因此新建的子实例,就拥有了父实例的所有属性,这就是继承属性的原理。对chi1和chi2的like属性,是每一个实例本身的属性,两者间不存在引用依赖关系,因此操做chi.like并不会对chi.like形成影响。方法继承,就是上文讲的到的原型链机制继承,另外能够给子构造函数添加本身的属性和方法。
这就是经典继承,避免了可是使用构造函数或者单独使用原型链的缺陷,成为js中最经常使用的继承方式。
用法: obj.hasOwnProperty(prop)
使用hasOwnProperty()方法能够判断访问的属性是原型属性仍是实例属性,若是是实例属性返回true,不然返回false
function Person() { } Person.prototype.name = '张三' var p1 = new Person(); var p2 = new Person(); p1.name = '张三'; console.log(p1.hasOwnProperty('name')) //true console.log(p2.hasOwnProperty('name')) //false
在实际开发中,若是原型对象有不少方法,每每咱们可使用字面量的形式,重写原型,可是须要手工指定constructor属性
function Person(name, age) { this.name = name; this.age = age; } var p1 = new Person('张三', 16); Person.prototype.showName = function() { return this.name; } Person.prototype.showAge = function() { return this.age; }
若是构造函数的prototype方法不少,能够采用字面量方式定义
Person.prototype = { constructor: Person, showName: function() { return this.name; }, showAge: function() { return this.age; } }
注意这里面手动加了一个constructor属性指向Person构造函数,这是由于使用字面量重写原型对象,这个原型对象变成了一个Object的实例,原型对象自己已经不存在最初函数建立时初始化的constructor属性,这是原型对象的[[prototype]]指针指向了Object.prototype
function Person() { } Person.prototype.a = 10; var p = new Person(); console.log(p.a) //10 Person.prototype = { constructor: Person, a: 20, b: 30 } console.log(p.a) // 10 console.log(p.b) // undefined var p2 = new Person(); console.log(p2.a) // 20 console.log(p2.b) // 30
所以,有的文章说“动态修改原型将影响全部的对象都会拥有新的原型”是错误的,新原型仅仅在原型修改之后的新建立对象上生效。
这里的主要规则是:对象的原型是对象的建立的时候建立的,而且在此以后不能修改成新的对象,若是依然引用到同一个对象,能够经过构造函数的显式prototype引用,对象建立之后,只能对原型的属性进行添加或修改。
以上就是我梳理出来的js中面向对象部分的相关概念和理解,依旧主要参考《JavaScript高教程》和《深刻理解JavaScript系列》文章,另外翻看了ECMAScript5.1中文版。本人对引用书中的概念和相关知识,为保证文章不误导你们,并非拿来主义,但愿本文能对你们有帮助,也但愿你们多多指教。