面向对象--对象的创建
对象的创建
1. Object() 构造函数
var foo = new Object();
2. 对象字面量
var foo = {name:"name",age:17};
以上两种方式 的缺点是: 在创建多个对象时, 会产生大量的重复代码.
3. 工厂模式
将公共的部分提取出来 , 用来创建相似的对象.
示例 :
function createPerson(name , job , age){
var o = new Object();
o.name = name ;
o.age = age ;
o.job = job ;
o.sayName = function(){
// do something ..
};
return o ;
}
这种模式 , 解决了创建相似对象 , 代码重复的问题
但是 , 无法检查 它创建的对象到底是什么类型的. 而且代码耦合性太强 ..
4. 构造函数模式
示例 :
function Person(name , job , age){
this.name = name ;
this.age = age ;
this.job = job ;
this.sayName = function(){
// do something ..
};
}
var person = new Person("name", "worker" , 20) ;
console.log(person instanceof Person); // display true
这种方式 , 与工厂模式的不同之处 :
a. 没有显示创建对象. 如 var o = new Object();
b. 直接将属性和方法 赋值给了 this
c. 没有return 语句
d. 函数名 最好要首字母大写
e. 使用时,必须使用 new 操作符.
构造函数 也是 函数的一种 , 因此也可以不使用 new 操作符.
Person("name", "worker" , 20); // 添加到window
window.sayName();
// 在指定作用域中调用
var o = new Object();
Person.call(o,"name", "worker" , 20); //添加到 对象o上.
o.sayName();
使用构造函数模式的缺点是:
使用构造函数创建的实例, 它们包含的内部函数虽然同名, 但实际上是不同的两个函数(即使处理逻辑是一样的)
对于同样的方法和逻辑,没有实现抽象,每个实例中都要创建一份
如果在全局中定义一个函数 , 让构造函数引用它 , 又毁坏了封闭性.
如果需要多个对象方法呢 , 总不能定义许多全局函数吧.
且这个可能只能被指定的对象 使用的函数(比如函数内耦合了this.personName),这个'全局函数'的名称也忒名不副实了.
========= 以上需要了解 , 以下模式 开始划重点 =========
5. 原型模式
每个由构造器模式声明的自定义类型内部都有个prototype属性, 它是个指针, 指向原型对象.
原型对象用来包含特定类型(即你创建对象的类型)的 被所有该类型的实例所共享的属性和方法.
换句话讲, 可以不用在构造函数中定义 对象实例的信息.将这些信息添加到原型对象上.
示例 :
function Person(){}
Person.prototype.name = "xx";
Person.prototype.age = 20 ;
Person.prototype.sayName = function(){
console.log(this.name);
};
理解 "原型" :
任何函数(不只是构造函数)在创建的时候, 都会默认生成 prototype 属性 , 它是原型对象的指针.
-- 因此,为了方便理解 下面再提到 Xxxx.prototype 就是指 原型对象 本身,而不是指 Xxxx 的prototype 指针. Xxxx.prototype 的返回值本来就是原型对象.
当使用构造函数(它就是函数)的时候, 同样有个prototype指针 指向 原型对象.
例如 : Person的prototype指针 指向了 Person.prototype(这里代指Person的原型对象).
当这个 原型对象 创建后 , 默认只有一个constructor属性(即 Xxxx.prototype.constructor , 也是个指针),
-- 原型对象创建的时机,就是每次调用构造函数的时候.
它指向了所属原型对象的所在的函数, 比如 Person.prototype.constructor => Person
再捋一遍路径 , 是个环形的 :
Person() --> prototype属性 --> Person.prototype --> Person.prototype.constructor --> Person
除了默认的constructor属性, 原型对象上 还会包含后添加的 自定义的属性和函数.
当使用构造函数 创建对象实例的时候, 对象实例 默认会有个指针(即__proto__属性) 指向了原型对象(Xxxx.prototype)
现在有了两个东西可以指向原型对象了, 分别是: Object 的prototype 以及 object 的__proto__ -- Object 和 object 分别代指 类 和 对象.
好, 这时候就可以捋一下 访问 对象的属性和函数 的查找链了 , 用Person来举例:
person --> person 的 __proto__ --> Person.prototype --> Person.prototype.constructor --> Person
从对象本身找 (如果没找到) 通过 __proto__ 属性,找到原型对象,看看它有没有 (如果没找到) 通过原型对象的constructor 看看Person 构造函数里有没有.
注意, 虽然对象实例可以访问到原型对象中保存的值, 但是不可以改变它里面的值. (经测试,这行不对,是可以改变的)
比如对象实例中定义了一个与原型对象中同名的属性,这会屏蔽原型中的同名属性,而不会覆写它,也就是说不会影响到其他对象.
对象实例会从Object那里继承一个方法 hasOwnProperty(propertyName), 该函数来检查 对象实例 本身中是否存在该属性, 如果存在则返回true.
原型 与 in 操作符
in 操作符无论单独使用, 还是在for-in语句中 , 都可以返回对象实例以及原型对象中的可枚举的属性/函数
因此 可以利用 hasOwnProperty 与 in 配合使用, 来查看 某个指定的属性 是否在原型对象中存在且没有被屏蔽
function hasPrototypeProperty(target,propertyName){
return !target.hasOwnProperty(propertyName) && (propertyName in target);
}
获取对象上的属性
Object.keys(target)
返回指定对象上的 所有可枚举的 属性; 如果该对象不是原型对象 , 该函数也不会通过__proto__向上查找.
Object.getOwnPropertyNames(target)
返回对象上所有属性 , 无论可不可枚举 ; 同样不会通过__proto__向上查找.
更简单的原型写法
即使用字面量形式来书写.
但是有个问题是,这时会默认设置它的constructor指向Object,而不是当前构造函数.(不过不影响正常使用instanceof,会得到正确结果)
为解决这个问题,可以显式声明.但是显式声明会让contructor的枚举特性设置成true.
所以如果有需要,最好使用Object.defineProperty()来设置一下constructor.
示例:
function Person(){}
// var p = new Person();
Person.prototype = {
name:"name",
age:20,
sayName : function(){
console.log(this.sayName);
}
};
Object.defineProperty(Person.prototype,"constructor",{enumerable:false});
// p.sayName();
如果上面的示例中 的注释去掉. 调用p.sayName()会引发错误.
因为 对象实例 和 构造函数没有直接关系, 只是刚好分别拥有了指向 原型对象的指针而已,这些指针是相互独立的.
当创建了对象的时候,两个指针所指向的对象是同一个原型对象.
当Person的prototype指向了 别的原型对象的时候, 对象实例的__proto__依然指向原来的原型对象,并不会受到影响.
别的原型对象上添加的属性/方法,自然无法体现在原来的原型对象上,从而不会影响到对象实例.
下面的示例,就不会报错 :
function Person(){}
var p = new Person();
Person.prototype.name="xxx";
Person.prototype.sayName = function(){console.log(this.name)} ;
p.sayName(); //在p上调用时,查看自身和原型上有没有该方法 -- 只要调用前,在prototype添加过该函数即可.
所有原生的引用类型都是使用这种模式创建对象的.例如 Array,String,Date等
可以通过原生对象的原型对象的引用 来修改/新增 原生对象上的属性/方法.
当然了,不到万不得已,这种方式是不推荐的.会影响在其他环境中造成冲突(即你也改原型,我也改原型,造成冲突和覆盖)
使用原型模式的缺点:
当原型对象上的属性是函数或者基本数据类型时,是没有问题的.对象可以屏蔽这个属性/函数.
但是,当这个属性是个引用类型时,如果是简单的引用赋值还好(即用=改变指针的指向),
如果使用了操作函数,会相互影响(比如数组,使用push).
如果你的初衷是不想让它共享,那这就是个问题了.
6. 组合使用构造函数模式和原型模式
创建自定义类型的最常见方式.
构造函数里定义实例属性,而原型对象里定义方法和共享属性
示例:
function Person(name , age){
this.name = name ;
this.age = age ;
}
Person.prototype = {
constructor : Person,
legsCnt : 2,
handsCnt : 2,
headCnt : 1,
speak: function(language){
return `i speak ${language}`;
}
};
7. 动态原型模式
示例:
function Person(name , age){
this.name = name ;
this.age = age ;
// 判断条件 只需要找 一个 原型对象 创建完成后必定存在的方法即可 -- 找一个就行
if(typeof this.speak !== "function"){
Person.prototype.speak = function(language){
return `i speak ${language}`;
};
Person.prototype.play = function(){ ... };
}
}
这样可以保证,这些方法只加载一次.这种方法 不能用字面量形式重写原型对象.
8. 寄生构造函数模式
本身和工厂模式没有不同, 只不过调用的时候使用了new操作符
示例 :
function createPerson(name , age){
var o = new Object();
o.name = name ;
o.age = age ;
o.sayName = function(){
// do something ..
};
return o ;
}
var p = new createPerson('窦唯’60);
直接看看用途吧 , 比如创建一个具有额外方法的数组,由于不能直接改Array的构造函数,就可以使用这个模式.
示例:
function SpecialArray(){
var array = new Array();
array.push.apply(array,arguments);
array.toPipedString = function(){
return this.join("|");
}
return array ;
}
var arr = new SpecialArray([1,2,3,4,5,6]);
arr.toPipedString(); // display 1|2|3|4|5|6
这种模式的问题是,不能依赖instanceof来判断这个实例的真实类型.
9.稳妥构造函数模式
所谓稳妥,就是在构造函数内部不使用this,调用函数的时候不使用new操作符(像调用普通函数一样调用)
这种模式适合在一些特殊环境中(这种环境不允许使用this和new:比如ADSafe中,Mashup程序中等)
示例:
function Person(name){
var o = new Object();
var name = name ;
var func = function(arg){
// do something
console.log(`arg is ${arg}`);
};
o.method = function(age){
console.log(name);
func();
}
return o ;
}
var p = Person();
p.method(20);