面向对象的程序设计-创建对象
面向对象语言的标志:都有类的概念,通过类可以创建任意多个具有相同属性和方法的对象。
JS中的对象:无序属性的集合,其属性可以包含基本值、对象或者函数。严格来说对象就是一组没有特定顺序的值,对象的每个属性或方法都有一个名字,每个名字都映射到一个值。可以把对象想象成散列的表:一组名值对。其中值可以是数据或者函数。
每一个对象都是基于一个引用类型创建的。
创建对象
创建自定义对象最简单的方式就是创建一个object的实例,然后再为他添加属性和方法。
构造函数模式创建对象
JS 中的构造函数可用来创建特定类型的对象,像Object和Array这样的原生构造函数。也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Person(name, age, job){ this.name = name, //将属性和方法赋给了this对象 this.age = age, this.job = job, this.sayName = function(){ console.log(this.name); }; } var person1 = new Person("wxc", 25, "engineer"); var person2 = new Person("ylg", 26, "engineer"); person1.sayName(); //"wxc" person2.sayName(); //"ylg" 要创建Person的新实例,必须使用new操作符,以这种方式调用构造函数实际上会经历以下4个步骤: 1、创建一个新对象; 2、将构造函数的作用域赋给新对象(this就指向了这个新对象); 3、执行构造函数中的代码(为这个新对象添加属性); 4、返回新对象; person1和person2分别保存着Person的一个不同的实例,这2个对象都有一个constructor(构造函数)属性,该属性指向Person。 console.log(person1.constructor == Person); //true console.log(person2.constructor == Person); //true constructor最初是用来标识对象类型的,但还是instanceof更可靠一些 console.log(person1 instanceof Object); //true console.log(person1 instanceof Person); //true 这个例子中创建的所有对象既是Object的实例,也是Person的实例。是因为所有对象均继承自Object。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
1、将构造函数当做函数
构造函数与其他函数唯一的区别就在于调用他们的方式不同。但构造函数也是函数,任何函数只要通过new操作符来调用,那他就可以作为构造函数;而任何函数如果不通过new调用,那他跟普通函数一样。
前面的例子中定义的Person()函数可以通过下列任何一种方式来调用:
//当做构造函数使用
var person = new Person("wxc", 25, "engineer"); //构造函数的典型用法,即使用操作符来创建一个新对象 person,sayName(); //"wxc"
//作为普通函数调用
Person("wxc", 25, "wxc"); //添加到window 当在全局作用域中调用一个函数时,this对象总是指向Global对象。 window.sayName(); //"wxc"
//在另一个对象的作用域中调用
var o = new Object(); Person.call(o, "wxc", 25, "engineer"); o.sayName(); //"wxc" //使用call()或者apply()在某个特殊对象的作用域中调用Person()函数,这里是在对象o的作用域中调用的,因此调用后o就拥有了所有的属性和方法。
2、构造函数的问题
使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍,上例中person1和person2都有一个名为sayName()的方法。但这2个方法都不是同一个Function的实例。因为JS中的函数是对象,因此每定义一个函数也就实例化了一个对象。
此时的构造函数也可以定义为:this.sayName = new Function("console.log(this.name)"); //与声明函数在逻辑上是等价的;
从这个角度看构造函数更容易明白每个Person实例都包含一个不同的Function实例(以显示name属性)的本质;
console.log(person1.sayName == person2.sayName);//false 从这可以证明这两个函数是不相等的;而创建2个完全同样任务的Function实例没必要,且有this对象在,就不用在执行代码前就把函数绑定到特定的对象上面。因此,通过把函数定义转移到构造函数外部来解决这个问题:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName ; //在构造函数内部将sayName属性设置成等于全局的sayName函数。这样由于sayName包含的是一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数 } function sayName(){ console.log(this.name); } var person1 = new Person("wxc", 25, "engineer"); //"wxc" var person2 = new Person("ylg", 26, "engineer"); //"ylg"
虽然这样确实解决了2个函数做同一件事的问题,但在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。且如果对象需要定义很多方法,那就要定义很多个全局函数,此时这个自定义的引用类型就丝毫没有封装性可言。这些问题可以通过使用原型链模式来解决。
原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,用途是包含可以由特定类型的所有实例共享的属性和方法。若按字面意思 prototype就是通过调用构造函数而创建的那个对象的原型对象。使用原型的好处是可以让所有对象实例共享它所包含的属性和方法,即不必在构造函数中定义对象信息,而是可以将这些信息直接添加到原型对象中。
function Person(){} Person.prototype = { constructor: Person, name: "wxc", age: 25, job: "enginner", sayName: function(){ console.log(this.name); } };
将Person.prototype设置为等于一个以对象字面量形势创建的新对象,最终结果相同,但constructor属性不再指向Person了,
因为每创建一个函数就会同时创建它的prototype对象,这个对象也会自动获得constructor属性,而我们在这使用的语法,
本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。
虽然用instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了。如下:
var person = new Person();
console.log(person instanceof Object);//true
console.log(person instanceof Person);//true
console.log(person constructor Objeft);//true
console.log(person constructor Person);//flase
如果constructor的值真的很重要,则可以将它特意的设置回适当的值。即constructor: Person
1、理解原型
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,默认情况下所有prototype属性会自动获得一个constructor(构造函数)属性,该属性包含一个指向prototype属性所在函数的指针。以前面例子来说,Person.prototype.constructor指向Person。而通过这个构造函数我们还可继续为原型添加其他属性和方法。
创建了自定义的构造函数之后,其原型属性默认只会取得constructor属性,其他方法都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内容将包含一个指针(内部属性,名字是__proto__),指向构造函数的原型属性。重要的是这个连接存在于实例与构造函数的原型属性之间,而不是存在于实例与构造函数之间。
上图展示了Person构造函数、Person的原型属性以及Person现有的2个实例之间的关系。Person.prototype指向原型对象,而Person.prototype.constructor又指回了Person。原型对象中包含constructor属性之外还包含后来添加的其他属性。Person的每个实例Person1和Person2都包含一个内部属性,该属性仅仅指向Person.prototype,即他们与构造函数没有直接的关系。格外注意的是,虽然2个实例都不包含属性和方法,但我们可以调用Person1.sayName()。这是通过查找对象属性的过程来实现的。
用原型对象的isPrototypeOf()方法测试实例内部是否有指针指向原型属性:
alert(Person.prototype.isPrototypeOf(person1)); //true
代码读取对象的某个属性时都会执行一次搜索,目标是具有给定名字的属性,搜索首先从实例本身开始,若存在则返回该实例的值,若未找到则继续搜索指针指向的原型对象。这正式多个对象实例共享原型所保存的属性和方法的基本原理。
虽然可通过对象实例访问保存在原型中的值,但不能通过对象的实例重写原型中的值,如果实例中添加了与原型中同名的属性,该属性将会屏蔽原型中的那个属性,但不会修改那个属性。
function Person(){} Person.prototype = { constructor: Person, name: "wxc", age: 25, job: "engineer"; sayName: function(){ console.log(this.name); } } var person1 = new Person(); var person2 = new Person(); person1.name = "henji"; console.log(person1.name); //"henji"; 来自实例 console.log(person2.name); //"wxc"; 来自原型
即使将这个属性设置为null,也只会在实例中设置这个值,而不会恢复其指向原型的连接,不过使用delete可以完全删除实例属性,从而能够重新访问原型中的属性:
delate person1.name; console.log(person1.name); //wxc 来自原型
使用hasOwnProperty()(是从Object继承来的)方法可以检测一个属性是存在于实例中还是原型中,这个方法只在给定属性存在于对象实例中时才返回true。
console.log(person1.hasOwnProperty("name)); //false
只有当Person1重写name属性后才会返回true,因为只有这个时候name才是一个实例属性,而非原型属性。
2、原型与in操作符
单独使用:in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
console.log("name" in person1); //true 来自原型 person1.name = "henji"; console.log("name" in person1); // true 来自实例
for-in循环中使用:返回的是所有能够通过对象访问的、可枚举的属性,其中包括存在于实例中的也包括原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的。
var o = { toString : function(){ return "my object"; } } for (var prop in o){ if(prop == "toString"){ alert("Fond toString"); //在IE中不会显示 } } 应该会显示一个警告框,表明找到了toString()方法
3、原型的动态性
因为在原型中查找值的过程是一次搜索,因此对原型对象所做的任何修改都能够立即从实例上反映出来--即使是先创建了实例后修改原型也会如此。
var person = new Person(); Person.prototype.sayHi = function(){ aconsole.log("hi); } person.sayHi(); //"hi" 没有问题 即使person实例是在添加新方法之前创建的,但任然可以访问这个新方法。因为实例与原型之间的松散连接关系,当调用sayHi()时,先在实例中搜索名为sayHi的属性,未找到的情况下会继续搜索原型。
虽然可随时为原型添加属性和方法,但如果重写整个原型对象则情况就不一样了。如:
var person = new Person(); Person.protoype = { constructor: Person, name: "wxc", age: 25, sayName: function(){ console.log(this.name); } }; person.sayName(); //error 先创建了Person的一个实例,然后重写了其原型对象,因为person指向的原型中不包含以该命名的属性。 我们都知道,调用构造函数时会为实例添加一个指向最初原型的__proto__指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。
切记:实例中的指针仅指向原型,而不指向构造函数。
4、原型对象的问题
原型模式也不是完全没有问题,首先省略了构造函数传递初始化参数一环,结果所有实例在默认情况下都将取得相同的属性值。但这不是原型的最大问题,最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的:
对于函数——非常合适
对于包含基本值的属性——可通过实例上添加一个同名属性隐藏原型中的对应属性
对于包含引用类型值的属性——问题就比较突出了,如下:
function Person(){} Person.prototype = { consttuctor: Person, name: "wxc", firends: ["111","222"], sayName: function(){ console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("333"); console.log(person1.friends); //"111,222,333" console.log(person2.friends); //"111,222,333" console.log(person1.friends == person2.friends); //true 由于friends数组存在Person.prototype而非person1中,所以向person.firends引用的数组中添加了一个字符串,这个修改也会通过person2.friends反映出来。 而这个问题正是我们很少看到有人单独使用原型模式的原因。
5、组合使用构造函数模式和原型模式
构造函数模式——用于定义实例属性
原型模式——用于定义方法和共享属性
结果:每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存,另外这种混合模式还支持向构造函数传递参数。下面代码重写前面例子:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["111","222"]; } Person.prototype = { constructor: Person, sayName: function(){ console.log(this.name); } }
var person1 = new Person("wxc", 29, "haha");
var person2 = new Person("ylg", 29, "heihei");
person2.friends.push("333");
console.log(person1.friends); //111 222
console.log(person2.friends); //111 222 333
console.log(person1.sayName === person2.sayName) //true
6、动态原型模式
动态原型模式把所有信息都封装在构造函数中,在必要时通过构造函数初始化原型,保持了同时使用构造函数和原型的优点:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.asyName != 'function'){
Person.prototype.sayNane = function(){
console.log(this.name);
}
}
}