创建复杂对象

445 阅读4分钟

虽然现在的开发都是直接使用ES6classTypeScript的 接口定义、 装饰器 等操作,但是了解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操作符调用就是构造函数,否则就是普通函数

按照管理,构造函数名称的首字母都是要大写的(借鉴于面向对象编程语言)。

自定义构造函数

比如ObjectArray这样的原生构造函数,运行时可以直接在执行环境中使用

也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法,确保实例被标识为特定类型

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

以上代码把属性和方法都直接添加到了Personprototype属性上。而实例间访问的都是相同的属性和相同的方法。

理解原型&原型链

原型模式的问题

首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值

最主要问题源自它的共享特性 —— 包含引用值的属性,在一个实例上修改这个引用值属性,会导致其他实例上的对应属性跟着改变。但是一般来说,不同的实例应该由属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因

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']