一 工厂模式创建对象
在 ECMAScript 中可以使用工厂模式来创建对象
function createPerson(name,age) {
let obj = new Object();
obj.name = name;
obj.age = age;
obj.sayHello = function () {
console.log("hello");
};
return obj;
}
let person = createPerson(“acey",22);
person.sayHello();
createPerson
函数可以接收参数来构造对象所需要的信息,可以被无数次的调用,每次调用都会返回一个唯一的对象,但我们无法辨认出我们创建的对象是什么类型。随着 JavaScript 的发展,构造函数模式就出现了。
二 构造函数模式
使用构造函数模式改造前面的栗子
function Person(name,age) {
this.name = name;
this.age = age;
this.sayHello = function () {
console.log("hello");
};
}
let person1 = new Person("acey1",22);
let person2 = new Person("acey2",22);
在这个栗子中, Person
函数取代了 createPerson
函数,构造函数模式创建对象的特点在于
- 没有显示的创建对象
- 直接将属性和方法赋值给
this
- 没有
return
语句
使用构造函数创建对象,在实例化对象的时候必须使用 new
来创建,创建的过程如下
- 创建一个新对象
- 将构造函数的作用域赋值给该对象(
this
就指向了该对象) - 执行构造函数中的代码
- 返回新对象
其中 person1
和 person2
分别着 Person
的一个不同的实例,它们都有一个 constructor
(构造器)属性,且该属性都指向 Person
console.log(person1.constructor === Person); //true
console.log(person2.constructor === Person);//true
构造函数和普通函数最大的区别就是创建的方式使用了 new
关键字
//构造函数
let person1 = new Person("acey1", 22);
/普通函数
Person("acey", 22);
person1.sayHello();
sayHell(); //Global
使用 new
来创建一个函数时,this
会指向创建的对象本身(Person),如果不用 new
创建,那么 this
会指向 Global
对象(在浏览器中就是
window
对象),这个时候我们就可以全局调用。
构造函数虽然好用,但还是存在一个问题,当我们每创建一个对象,那么对象中的方法也会被重新创建一遍,这样就会导致不同的作用域链和标识解析。
console.log(person1.sayHello() === person2.sayHello())//false
想要避免这种情况,我们需要将对象中的方法全局化,这个时候可以使用原型模式来创建对象。
三 原型模式
浅析
我们创建的每一个函数都会有一个 prototype
(原型)属性,这个属性是一个指针,指向一个对象,这个对象的作用就是包含由特定类型的所有实例所共享的属性和方法。
比如前面创建 person1
函数时,该函数就会有一个 prototype
属性,而这个属性指向的对象就是 Person
对象。
使用原型模式改写上面的栗子
function Person() {
}
Person.prototype.name = "acey";
Person.prototype.age = 22;
Person.prototype.sayHello = function () {
console.log("hello");
};
let person1 = new Person();
person1.sayHello(); //hello
let person2 = new Person();
person2.sayHello();//hello
console.log(person1.sayHello === person2.sayHello);//true
在这我们将name、age、sayHello
直接添加到了 Person
中的prototype
属性中,person
中 prototype
中的属性都被 Person
的实例化对象所共享。
理解原型对象
每当我们创建一个函数,就会根据一种特定的规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有的原型对象都会有一个 constructor
属性,这个属性指向 prototype
属性所在的函数的指针。Person.prototype.constructor
就指向 Person
当调用一个构造函数创建一个实例后,该实例就会获得一个指针叫[[Prototype]](内部属性),该指针指向构造函数的原型对象。在 FireFox、Safari 和 Chrome 是 _proto_
。但在其它实现中这个属性是完全不可见的。要记住一点就是,这个连接是存在实例与构造函数的原型对象之间的,而不是实例与构造函数之间的
我们可以使用 isPrototypeOf()
方法来判断对象是否是同一个原型对象
console.log(Person.prototype.isPrototypeOf(person1));//true
在 ECMAScript 5 中有一个方法 Object.getPrototypeOf()
可以返回某个对象的原型
console.log(Object.getPrototypeOf(person1).name); //acey
属性的搜索
每当读取某个属性时,都会进行一次搜索。搜先会搜索对象实例本身,如果搜索到了该具体名字的属性,就会返回该属性的值,如果没有搜索到,那么就会继续搜索指针指向的原型对象,在原型对象中如果搜索到的话就会返回属性值。
可以发现,在搜索的时候是先搜索的是对象实例本身,然后才是原型对象。如果在对象本身和原型对象含有相同的属性,那么原型对象中的属性和方法就会被对象实例中相应的属性和方法所覆盖。(可以使用 delete
操作符来删除对象中的属性和方法, delete person1.name;
)
相关方法
-
hasOwnProperty()
:检测某个属性是否存在与某个实例中。
person2.hasOwnProperty("sayHello");//true
-
in
操作符:检测属性是否存在与对象实例或是原型对象中
console.log("name" in person1);//true
-
Object.keys()
:接收一个对象作为参数,返回该对象所有可枚举属性的字符串数组
let keys = Object.keys(Person)
原型对象存在的问题
原型对象省略了构造函数传递参数初始化的步骤,结果导致所有的实例都会共享相同的属性,这是非常不方便的。如果创建的属性而引用类型的话,那么会造成不同的实例的混乱。
四 组合使用构造函数和原型模式
最佳的方式应该是这样。构造函数用来定义属性,而原型模式定义共享的属性和方法。这样每个实例都会有一份实例属性副本,又同时含有一份共享的属性和方法,这样最大限度的节省了内存。上面的栗子改写成
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.age)
};
let p1 = new Person("acey", 22);
let p2 = new Person("acey1", 22);
console.log(p1.say === p2.say);//true
当然了这种方式并不是很完美的,因为构造函数和原型两个独立起来了。我们动态原型模式将其合并起来。
function Person(name,age) {
this.name = name;
this.age = age;
if(this.say === undefined) {
Person.prototype.say = function () {
console.log(this.age);
}
}
}