ES5并没有正式支持面向对象的结构,比如类或者继承,我们可以采用原型链的方式实现继承。ES6正式支持类和继承,ES6的类设计也仅仅是封装ES5的构造函数和语法糖。
采用面向对象模编程还是应该使用ES6的类
工厂模式
function createPerson(name, age, job) {
const obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName=function () {
console.log(this.name);
};
return obj;
}
let p1 = createPerson('章三',18,'测试')
createPerson()接受三个参数,这几个参数共同组成了一个对象。每次都会返回一个包含三个属性的对象。这种虽然可以解决可以创建多个类似对象的问题,但没有解决对象标识的问题,即新创建的对象是什么类型的(通过打印可以发现,返回的都是Object)。
构造函数模式
构造函数可以用来创建特定类型的对象,Object、Array这样原生的构造函数可以在运行时直接使用。我们也可以自己定义自己的构造函数来创建相关的对象及其属性。
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
}
}
let p1 = new Person('王五', 1, '测试')
打印p1对象,可以看到对应的属性已经有了,同事构造函数指向的也是Person。按照惯例构造函数的首字母要大写。使用构造函数创建对象需要使用new关键字,该方法的执行过程如下:
- 在内存中创建一个新对象
- 这个对象的内部[[Prototype]]特性被赋值为构造函数的prototype(也就是我们常说的对象隐式原型与它的构造函数显示原型是相等的即 p1.ptoto=Person.prototype )
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)
- 执行构造函数内部的代码(即给新对象添加属性)
- 如果构造函数返回非空对象,则返回的是该对象;否则返回新创建的对象
构造函数也是函数
构造函数与普通函数的唯一区别在于:调用方式不一样。 除此之外构造函数也是函数,并没有把某些函数定义为构造函数的方法。只要被new关操作符调用的函数就是构造函数。
new Person('章三',1,'测试') --作为构造函数使用
Person('李四',1,'测试' --作为函数 添加到window对象中)
let o = new Object(); Person.call(o,'李四',1,'测试') --在其他对象作用域使用,指定this到对象o中
构造函数的问题
构造函数的主要问题:其定义的方法会在每个实例上都创建一遍。因此如果我们基于上面Person的例子,创建多个p1、p2等对象,那么每个对象都有一个sayName方法,但这两个方法不是同一个Function实例。构造函数实际如下:
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)") // 每次都创建新的函数
}
以这种方式创建函数会带来不同的作用域链和标识符解析, p1.satName()===p2.sayName()为false
。 因为函数都是做一样的事,所以没必要每次都创建,要解决这个问题,可以把函数的定义转移到构造函数的外部,代码如下:
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name)
}
原型模式
每个对象都会创建一个prototype属性,该属性是一个对象。实际上,这个对象就是通过调用构造函数创建对象的原型。使用原型对象的好处:在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋值给对象实例的值,可以直接赋值给他们的原型。
function Person() {}
Person.prototype.name = '章三';
Person.prototype.age = 18;
Person.prototype.sayName=function () {
console.log(this.name);
};
let p1 = new Person();
let p2 = new Person();
p1.sayName() === p2.sayName() // true
理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有原型对象自动获得一个constructor的属性,指回与其关联的构造函数,Person.prototype.constructor指向Person。因构造函数而异,可以给原型对象添加其他的属性。
在自定义构造函数时,原型对象默认只会获取constructor属性,其他所有方法都是继承自Object。每次调用构造函数创建一个实例对象,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象 ,实例对象可以通过__proto__进行访问,通过这个属性可以访问对象的原型。实例与构造函数原型有直接联系,但实例与构造函数没有联系。
上图我们可以发现这个原型对象里有一个constructor,引用了这个构造函数,也就是说两者循环引用Person.prototype.constructor===Person
实例可以通过__proto__链接到原型对象,它实际是指向隐蔽特性[[Prototype]],构造函数通过prototype属性链接到原型对象,实例与构造函数没有直接联系,与原型对象有联系。
同一个构造函数创建的不同实例,其共享同一个原型对象
可以使用instanceof检查实例的原型链中是否包含指定构造函数的原型。
当我们使用下面的操作时,Person.prototype.constructor就不会在指向Person了
function Person() { }
Person.prototype={
name: '原始人',
age: 100,
}
// Person.prototype={
// name: '原始人',
// constructor: Person --手动指定constructor,这样就会指向了
// age: 100,
//}
let p1 = new Person()
该写法完全重写了默认的prototype,因此constructor也指向了完全不同的新对象,此时不可以在通过constructor来识别类型了。
Person.prototype指向原型对象,Person.prototype.constructor指回Person的构造函数。原型对象包含constructor及其后添加的属性。Person的两个实例p1、p2都有一个内部属性指回Person.prototype(隐式原型指显示原型)。虽然两个实例都没有方法,但是可以调用sayName()方法,这是对象属性查找机制(也就是原型链)。我们可以使用isPrototypeOf()方法确定两个对象之间的关系。
Person.prototype.isPrototypeOf(p1) // true,判断p1的原型是不是Person的显示原型,因为p1内部有__proto__指向显示原型。
Object.getPrototypeOf(p1)===Person.prototype // 获取p1的原型 与Person的进行对比
Object.setPrototypeOf(obj,参数) // 给obj的[[Prototype]]添加新的属性,该属性可能会严重影响性能,推荐使用Object.create()创建对象
原型层级
在通过对象访问属性时,会按照这个属性的名称开始搜索。先搜索实例本身,如果存在则返回,不存在则会进入原型对象,如果在原型对象上找到值,则返回。所以p1.sayName()时,会发生两步搜索。虽然可以通过实例读取原型对象上的值,但不可能通过实例修改这些值,如果实例上添加了一个与原型相同名称的属性,则会返回实例上的。会遮住原型上的值。
function Person() { }
Person.prototype.name = '原始人'
Person.prototype.age = 100
let p1 = new Person()
let p2 = new Person()
p1.name = '现代人'
// 执行 delete p1.name 在输出p1.name