先来了解一些基础概念,才能更好的理解知识。chrome
对象的定义数组
ECMA-262把对象定义为:“无序属性的集合,其属性能够包含基本值、对象或者函数。
4个特性浏览器
以上属性通常经过Object.defineProperty()方法来修改,接受三个参数:属性所在的对象、属性的名字和一个描述符对象。其中描述符就是上面四个特性。app
4个特性函数
访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。学习
Object.defineProperties()方法优化
在调用 Object.defineProperty()方法时,若是不指定,configurable、enumerable 和
writable 特性的默认值都是 false。
Object.getOwnPropertyDescriptor()方法this
接受两个参数:属性所在的对象和要读取其描述符的属性名称。
var book = {}; Object.defineProperties(book, { _year: { value: 2004 }, edition: { value: 1 }, year: { get: function(){ return this._year; }, set: function(newValue){ if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); alert(descriptor.value); //2004 alert(descriptor.configurable); //false alert(typeof descriptor.get); //"undefined" var descriptor = Object.getOwnPropertyDescriptor(book, "year"); alert(descriptor.value); //undefined alert(descriptor.enumerable); //false alert(typeof descriptor.get); //"function"
用函数来封装以特定接口建立对象的细节spa
function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function () { alert(this.name); }; return o; } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor");
做用:解决了建立多个类似对象的问题
缺陷:没有解决对象识别的问题(即怎么知道一个对象的类型)prototype
new 一个构造函数一般会经历一下四个步骤
因为构造函数内部定义的属性和方法,经过实例化后都有本身的做用域链和辨识符解析。为了让属性和函数可以更好的公用,这时候原型模式就要登场了。
isPrototypeOf()的做用
当调用构造函数建立一个新实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262中管这个指针叫[[Prototype]]。在脚本中没有标准的方式访问[[Prototype]],但浏览器(Firefox,Safari,chrome)在每一个对象上都支持一个属性/__proto/__;咱们要知道的是,这个属性对脚本则是彻底不可见的。能够经过isPrototypeOf()方法来肯定对象之间是否存在这种关系。从本质上讲,若是[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //"Nicholas" Person.prototype.isPrototypeOf(person1)//true
ECMAScript 5 增长了一个新方法 Object.getPrototypeOf(),在全部支持的实现中,这个方法返回[[Prototype]]的值。例如
Object.getPrototypeOF(person1) === Person.prototype // true
hasOwnProperty()的使用场景
当咱们从一个构造函数实例一个对象后,若是在这个实例上建立一个属性或者方法,正好与这个实例的原型上的属性或者方法同名,那么访问时就会 屏蔽原型上的属性或者方法。固然这样作不会修改原型上的属性或者方法。若是要恢复指向原型,那么使用delete操做符则能够彻底删除实例属性。 使用 hasOwnProperty()方法能够检测一个属性是存在于实例中,仍是存在于原型中。 不要忘记这个方法是从Object继承来的,因此只在给定属性存在于对象实例中时,才会返回true.
in 操做符的做用
in 操做符只要经过对象可以访问到属性就会返回true. ,hasOwnProperty()只在属性存在于实例中时才返回 true,所以只要 in 操做符返回 true 而hasOwnProperty()返回 false,就能够肯定属性是原型中的属性。
function hasPrototypeProperty(object, name){ return !object.hasOwnProperty(name) && (name in object); } function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person = new Person(); alert(hasPrototypeProperty(person, "name")); //true person.name = "Greg"; alert(hasPrototypeProperty(person, "name")); //false
for-in的特殊
在使用 for-in 循环时,返回的是全部可以经过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为 false 的属性)的实例属性也会在 for-in 循环中返回,由于根据规定,全部开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外。
Object.keys()
获取对象上全部 可枚举的实例属性,返回一个字符串数组。
Object.getOwnPropertyNames()
若是是想获得全部的实例属性,包括不可枚举的,就能使用Object.getOwnPropertyNames() 可以获取到不可枚举的constructor属性。
constructor属性什么状况下会指向Object
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
为了减小重复代码,咱们常常用一个包含全部属性和方法的对象字面量来重写整个原型对象。代码如上所示。这里会产生一个状况,就是constructor属性再也不指向Person。为何会这样呢?咱们知道,每建立一个函数,就会同时建立它的prototype对象,这个对象也会自动得到constructor属性。而咱们上面这样的写法,本质上彻底重写了默认的prototype对象,所以constructor属性也就变成了新对象的constructor属性(指向Object构造函数),再也不指向Person函数。因此为何咱们用instanceof并不能准确的判别实例的构造函数。以下代码
var p = new Person() console.log(p instanceof Object)//true console.log(p instanceof Person)//true console.log(p.constructor === Person)//false console.log(p.constructor === Object)//true
有下面两种方法将constructor设置回来
function Person(){ } Person.prototype = { constructor : Person, name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
以上这种重设constructor属性会致使它的[[Ennumerable]]特性设置为true,因此咱们能够用下面这种方法;
//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
原型模式的缺点就是在共享时属性和方法后,要是修改了内容值,那么同一个构造函数出来的实例就会都受到影响。例以下面代码
function Person(){ } Person.prototype = { constructor: Person, friends : ["Shelby", "Court"] }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(person1.friends === person2.friends); //true
因此组合使用构造函数模式与原型模式就是你们广泛用到的方法,咱们想要定义本身的属性和方法,那么就在构造函数中定义便可。例以下面代码:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
意思就在初始化实例的时候就检测判断是否原型中存在你想要的属性或者方法,要是没有,就能够在原型上定义你想要的属性或方法。如一下代码:
function Person(name, age, job){ //属性 this.name = name; this.age = age; this.job = job; if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); }; } } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName()
这个模式跟工厂模式几乎是同样的。经过在一个构造函数内部建立一个新的对象(数组、字符串),而后你能够随意在这个定义的新对象上添加属性和方法,最后return这个新的对象。例如一下代码:
function SpecialArray(){ //建立数组 var values = new Array(); //添加值 values.push.apply(values, arguments); //添加方法 values.toPipedString = function(){ return this.join("|"); }; //返回数组 return values; } var colors = new SpecialArray("red", "blue", "green"); alert(colors.toPipedString()); //"red|blue|green"
这样作的优势是咱们能够很轻松在原生对象上建立一个本身想要的方法和属性。可是返回的对象与构造函数或者与构造函数的原型属性之间没有关系。这样咱们不能经过instanceof操做符肯定对象类型。
所谓稳妥对象,指的是没有公共属性,并且其方法也不引用this对象。一是新建立对象的实例方法不引用this;二是不使用new操做符调用构造函数。
咱们先来弄懂一句话: 每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。这就是至关于构建一个原型链的基石,整个原型链就是由这样一个基石累计起来。
主要有下面两个缘由
好比下面的例子中咱们实现简单的继承
function Father(){ this.colors = ["red","blue","green"]; } function Son(){} Son.prototype = new Father(); var instance1 = new Son(); instance1.colors.push('black'); console.log(instance1.colors);// red blue,green,black var instance2 = new Son(); console.log(instance2.colors);// red blue,green,black
上面例子咱们能够看见两个实例共享了一个方法,这是咱们不肯意看到的地方,那么有什么方法可以解决这个问题呢?
什么是 借用构造函数? 就是在子类型构造函数的内部调用超类型的构造函数。以下面代码:
function Father(){ this.colors = ['red','blue','green']; } function Son(){ //继承 或者使用apply Father.call(this) } var instance1 = new Son(); instance1.colors.push('black'); console.log(instance1.colors);//red blue,green,black var instance2 = new Son(); console.log(instance2.colors);// red blue,green
怎么传递参数? 就是在调用父类构造函数时,传递参数。具体见下面代码
function Father(name) { this.name = name; } function Son(){ Father.call(this,'pan'); this.age = 28; } var instance = new Son() console.log(instance.name)//pan console.log(instance.age)//28
可是借用构造函数也用的很少,为何呢?
上面两种继承都有本身的缺点和优势,那么咱们是否能够取长补短,把两家结合在一块儿?组合继承就是用来解决这个问题的。思路就是使用原型链实现对原型属性和方法的继承,而经过借用构造函数来实现对实例属性的继承。这样,即经过在原型上定义方法实现了函数复用,又可以保证每一个实例都有它本身的属性。
function Father(name) { this.name = name; this.colors = ['red','blue','green']; } Father.prototype.sayName = function(){ console.log(this.name) } function Son(name,age){ Father.call(this,name); this.age = age; } //继承的实现 Son.prototype = new Father(); Son.prototype.constructor = Son; Son.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Son('pan',28); instance1.colors.push('black'); console.log(instance1.colors);// red ,blue,green,black instance1.sayName();//pan instance1.sayAge();//28 var instance2 = new Son('lin',26); console.log(instance2.colors);// red ,blue,green instance2.sayName();//lin instance2.sayAge();//26
上面的例子咱们看到取长补短的优点,便可以定义本身的属性和方法,又可使用相同的方法。这是咱们经常使用的继承模式,而且instanceof和isPrototypeOf也可以用于识别基于组合继承建立的对象。
咱们先来看一个Object.create()的前身的写法,借助原型能够基于已有的对象建立新对象,同时还没必要所以建立自定义类型。
function object(o) { function F(){} F.prototype = o; return new F(); }
在一个function 内部建立一个临时性的构造函数,而后重写这个构造函数的原型,而后在返回这个构造函数的新的实例。不知道你又没有看出什么很差的地方没?从本质上讲,object()对传入其中的对象执行一次浅复制。
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
咱们用新的方法Object.create()来从新规范上门的例子。这个方法接收两个参数:一个做用新对象原型的对象和一个为新对象定义额外属性的对象。
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每一个属性都是经过本身的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person, { name: { value: "Greg" } }); console.log(anotherPerson.name); //"Greg"
若是只是简单的继承,那么原型模式就能胜任,可是想要建立构造函数,那么引用类型值的会共享这个坑是不能避免的。
都是寄生,天然和构造函数的寄生相似,建立一个仅用于封装继承过程的函数,该函数在内部以某种方式来增前对象,而后再返回这个对象。
function createAnother(original){ var clone = object(original); clone.sayHi = function(){ console.log('hi') } return clone; }
接下里咱们运用上面这个函数
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //"hi"
新对象anotherPerson不只具备person的全部属性和方法,并且还有本身的sayHi()方法。可是此寄生式继承不能复用函数,因此效率不是很高。
咱们前面说的组合继承是经常使用的模式,可是也有一个缺点:那就是会调用两次超类型构造函数。什么意思?咱们来看代码;
function Father(name) { this.name = name; this.colors = ['red','blue','green']; } Father.prototype.sayName = function(){ console.log(this.name) } function Son(name,age){ Father.call(this,name);//第二次调用Father() this.age = age; } //继承的实现 Son.prototype = new Father();//第一次调用Father() Son.prototype.constructor = Son; Son.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Son('pan',28); instance1.colors.push('black'); console.log(instance1.colors);// red ,blue,green,black instance1.sayName();//pan instance1.sayAge();//28
上面代码中两个注释很好的反应了这两次构造函数。那么寄生组合式继承就可以很好的优化这个问题。经过Object.create()来很好的实现,只须要修改一处便可。
将Son.prototype = new Father(); 替换成 Son.prototype = Object.create(Father.prototype)
高效率的体现就在只调用了一次Father构造函数,能够避免在Son.prototype上面建立没必要要的、多余的属性。与此同时,原型链还能保持不变。而且还能正常使用instanceof和isPrototypeOf()。
上面的寄生组合式继承只是一个 单继承,那 多继承又该如何实现?
咱们能够用混入的方式来实现多个对象的继承
//第一个构造函数 function Father(name) { this.name = name; this.colors = ['red','blue','green']; } Father.prototype.sayName = function(){ console.log(this.name) } //第二个构造函数 function FatherBrother(hobby){ this.hobby = hobby } function Son(name,age,hobby){ Father.call(this,name); FatherBrother.call(this,hobby) this.age = age; } //继承的实现 Son.prototype = Object.create(Father.prototype); //混合 Object.assign(Son.prototype,FatherBrother.prototype) //从新指定constructor Son.prototype.constructor = Son; Son.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Son('pan',28,'play'); instance1.colors.push('black'); console.log(instance1.colors);// red ,blue,green,black instance1.sayName();//pan instance1.sayAge();//28 instance1.hobby;//play
这里关键点是:Object.assign 会把 FatherBrother原型上的函数拷贝到 Son原型上,使 Son 的全部实例均可用 FatherBrother 的方法。
工厂模式(简单的添加属性和方法) -->构造函数模式(优势:能够建立内置对象实例。缺点:成员没法复用。)-->原型模式(结合构造函数模式和原型模式两方的优势,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。)-->原型式继承(优势:没必要预先定义。缺点:会存在重写原型)-->寄生式继承(优势:效率高,没必要屡次调用超类型构造函数。缺点:复用率不高)-->寄生组合式继承(集寄生式继承和组合继承的优势与一身。)
若是大神您想继续探讨或者学习更多知识,欢迎加入QQ一块儿探讨:854280588![]()
参考文章