虽然现在的开发都是直接使用ES6的class和TypeScript的 接口定义、 装饰器 等操作,但是了解ES6之前的 对象创建、继承实现 还是很有必要的(毕竟ES6的类也只是封装了构造函数+原型继承的语法糖而已)
创建复杂对象
工厂模式
用于抽象创建特定对象的过程
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
};
return o;
}
let person1 = createPerson('echo', 12, 'engineer');
let person2 = createPerson('Jon', 1, 'Doctor');
工厂模式的问题
这种模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)
而使用构造函数可以对实例进行标识
构造函数模式
ECMAScript中的构造函数就是能创建(特定类型)对象的函数。
构造函数 & 函数
构造函数也是函数,与普通函数唯一的区别就是调用方式不同。任何函数只要使用new操作符调用就是构造函数,否则就是普通函数
按照管理,构造函数名称的首字母都是要大写的(借鉴于面向对象编程语言)。
自定义构造函数
比如Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用
也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法,确保实例被标识为特定类型
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
}
}
let person1 = new Person('echo', 12, 'engineer');
let person2 = new Person('Jon', 1, 'Doctor');
person1.sayName(); // 'echo'
console.log(person1.constructor == Person); // true
// constructor本来是用于标识对象类型的
// 但是一般认为instanceof操作符时确定对象类型更可靠的方式
// 因为constructor的指向有可能被更改
console.log(person1 instanceof Person); // true
// 每个对象都是Object的实例,因为所有自定义对象都继承自Object
console.log(person1 instanceof Object); // true
在实例化时,如果不传参数,可以不加括号:let person = new Person;
Person()内部的代码与createPerson()的区别如下:
- 没有显示地创建对象
- 属性和方法直接赋值给了
this - 没有
return Person的首字母大写了
使用构造函数创建实例,应使用new操作符。以这种方式调用构造函数会执行如下操作:
1、在内存中创建一个新对象
2、这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的prototype属性(即当前对象的原型)
3、构造函数内部的this被赋值为这个新对象(即this指向新对象)
4、执行构造函数内部的代码
5、如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
在调用一个函数而没有明确设置this值的情况下(即没有作为对象的方法调用,或者没有使用call()/appy()调用),this始终指向Global对象(在浏览器中就是window对象)
Person('g', 20, 'Doctor'); // 添加到window对象
window.sayName(); // 'g'
let o = new Person();
Person.call(0, 'k', '10', 'Nurse');
o.sayName(); // 'k'
构造函数的问题
主要问题在于其定义的方法会在每个实例上都创建一遍,因此不同实例上的函数虽然同名却不相等,而实现的功能都是一样的。
解决(把函数定义移到构造函数外面):
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 = 'echo';
Person.prototype.sayName = function() {
console.log(this.name);
}
const person1 = new Person();
const person2 = new Person();
console.log(person1.sayName == person2.sayName); // true
以上代码把属性和方法都直接添加到了Person的prototype属性上。而实例间访问的都是相同的属性和相同的方法。
原型模式的问题
首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值
最主要问题源自它的共享特性 —— 包含引用值的属性,在一个实例上修改这个引用值属性,会导致其他实例上的对应属性跟着改变。但是一般来说,不同的实例应该由属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因
function Person() {}
Person.prototype = {
constructor:Person,
name: 'echo',
age: 12,
friends: ['Jon', 'May'],
sayName() {
console.log(this.name);
}
}
let person1 = new Person();
let person2 = new Person();
person1.friends.push('Van');
console.log(person1.friends); // ['Jon', 'May', 'Van']
console.log(person2.friends); // ['Jon', 'May', 'Van']