第四章:对象—上-灵析社区

懒人学前端

一、JavaScript 创建对象有哪些方式?

JavaScript 中有八种数据类型,有七种基本数据类型和对象(Object),对象就是引用类型。这里我们介绍八种创建对象的方式:

  • 三种最基本的方式

Object 构造函数

对象字面量

Object.create()

  • 类(语法糖)
  • 四种运用原型式继承的方式。ES6 开始正式支持类和继承,涵盖了之前规范设计的基于原型的继承模式。

工厂模式

构造函数模式

原型模式

组合模式

一、Object 构造函数

创建自定义对象可以创建 Object 的一个新实例,再添加属性和方法,如下所示:

let person = new Object();

// 添加属性
person.name = "Lucy";

// 添加方法
person.sayName = function() {
  console.log(this.name)
}

二、对象字面量

对象字面量创建新对象更为简单直接。如下所示:

let person = {
  name: "Lucy",
  sayName() {
    console.log(this.name)
  }
}

这个例子中的 person 对象和上文例子中的 person 对象是等价的,它们的属性和方法都一样。

三、Object.create()

Object.create()方法创建一个新的对象,使用现有的对象作为新创建对象的原型。

let person = {
  name: "Lucy",
  sayName() {
    console.log(this.name)
  }
}

let person1 = Object.create(person);
console.log(person1.name); // "Lucy"

四、类(ES6)

类用于创建对象的模版,它建立在原型上。类是“特殊的函数”,类语法有两个组成部分:类表达式和类声明。

我们用类声明来创建一个对象:

class Person {
  constructor(name) {
    this.name = name;
  }
	// 定义方法
  sayName() {
    console.log(this.name)
  }
}

const person1 = new Person("Lucy");
person1.sayName(); // "Lucy"

注意:函数声明和类声明的一个重要区别是:函数声明会提升,类声明不会。

我们用类表达式来创建一个对象,类表达式可以是命名或不命名的,如下用匿名类创建对象:

let Person = class {
  constructor(name) {
    this.name = name;
  }
	// 定义方法
  sayName() {
    console.log(this.name)
  }
}

const person1 = new Person("Lucy");
person1.sayName(); // "Lucy"

类相比原型模式,具备原型模式的优点,代码封装更好。

五、工厂模式

工厂模式是一种运用广泛的设计模式,用于抽象创建特定对象的过程。如下所示:

function createPerson(name) {
  let o = new Object();
  o.name = name;
  o.sayName = function() {
    console.log(this.name);
  };
  return o;
}

let person1 = createPerson("Lucy");
let person2 = createPerson("Joe");

函数 createPerson()接收 1 个参数 name, 根据这个参数创建了一个包含 Person 信息的对象,可以用不同的参数多次调用这个函数,每次都会返回包含 1 个属性和 1 个方法的对象。在 createPerson() 中可以根据实际需求给 Person 对象添加更多属性和方法。

它的优缺点如下:

  • 优点:可以解决创建多个类似对象的问题
  • 缺点:没有解决对象标识问题(即新创建的对象是什么类型,类型如 Array)

六、构造函数模式

构造函数是用于创建特定类型对象的。像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。我们可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

function Person(name) {
	this.name = name;
    this.sayName = function() {
        console.log(this.name)
    }
}

let person1 = new Person("Lucy");
let person2 = new Person("Joe");

person1.sayName(); // "Lucy"
person2.sayName(); // "Joe"

在这个例子中,Person() 构造函数代替了 createPerson() 工厂函数。实际上,Person() 内部的代码和 createPerson() 基本是一样的,只是有如下区别:

  • 没有显式地创建对象
  • 属性和方法直接赋值给了 this
  • 没有 return

另外,需要注意函数名 Person 大写了。按照惯例,**构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。**这是为了区别构造函数和普通函数,毕竟 ECMAScript 的构造函数就是能创建对象的函数。

要创建 Person 的实例,应使用 new 操作符,用 new 操作符调用构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
  4. 执行构造函数内部的代码
  5. 如果构造函数返回非空对象,则返回该对象,否则,返回刚创建的新对象。

那么 new 是如何具体实现的呢?我们可以模拟一个 new 的实现,由于 new 是个关键字,我们把构造函数当作函数的参数传入:

function newOperator(Constructor, ...args) {
  let thisValue = Object.create(Constructor.prototype);  // 对应上文操作步骤: 1、2
  let result = Constructor.apply(thisValue, args); // 对应上文操作步骤: 3、4
  return typeof result === 'object' && result !== null ? restult : thisValue; // 对应上文操作步骤: 5
}

// 测试代码

function Person(name) {
	this.name = name;
    this.sayName = function() {
        console.log(this.name)
    }
}

let person1 = newOperator(Person, "Lucy");
let person2 = newOperator(Person, "Joe");

person1.sayName(); // "Lucy"
person2.sayName(); // "Joe"

上文中创建的 person1 person2 都分别保存着 Person 的不同实例。这两个对象都有一个 constructor 属性指向 Person

console.log(person1.constructor === Person) // true
console.log(person2.constructor === Person) // true

constructor 是用于标识对象类型的,instanceOf 操作符也可以确定对象类型。如下所示:

console.log(person1 instanceof Object);  // true
console.log(person1 instanceof Person);  // true

自定义构造函数模式的优缺点如下:

  • 优点:可以确保实例被标识为特定类型,相比于工厂函数,这是一个很大的好处。
  • 缺点:定义的方法在每个实例上都创建一遍。z

这个缺点从上文的例子来看,person1person2 的都有名为 sayName() 的方法,但这两个方法不是同一个 Function 实例。我们知道,ECMAScript 中的函数是对象,因此每次定义函数时,都会初始化一个对象。逻辑上讲,这个构造函数实际上是这样的:

function Person(name) {
  this.name = name;
  this.sayName = new Function("console.log(this.name");
}

这样理解这个构造函数可以更清楚知道,每个 Person 实例都会有自己的 Function 实例用于显示 name 属性。当然了,以这种方式创建函数会带来不同的作用域链和标识符解析。但创建新 Function 实例的机制是一样的,因此不同实例上的函数虽然同名却不相等,如下所示:

console.log(person1.sayName === person2.sayName); // false

因为都是做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的绑定推迟到运行时。

要解决这个问题,我们可以把函数定义转移到构造函数外部:

function Person(name){
    this.name = name;
    this.sayName = sayName;
}
function sayName() {
      console.log(this.name);
}

let person1 = new Person("Lucy");
let person2 = new Person("Joe");

person1.sayName();  // "Lucy"
person2.sayName();  // "Joe"

sayName() 被定义到了函数的外部,构造函数内部,sayName 属性指向的是全局 sayName() 函数,所以 person1person2 共享了定义在全局作用域上的 sayName 函数。

虽然这样解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,代码也不能很好地聚集在一起。这个新问题可以通过原型模式解决。

七、原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。

function Person() {};

// 将属性和方法添加到 prototype 属性上
Person.prototype.name = "Lucy";
Person.prototype.sayName = function() {
  console.log(this.name)
}

let person1 = new Person();
person1.sayName(); // "Lucy"

let person2 = new Person();
person2.sayName(); // "Lucy"

八、组合模式

组合模式是原型模式和构造函数模式的结合。

function Person(name) {
  this.name = name;
}

Person.prototype = {
  constructor: Person,
  sayName: function() {
    console.log(this.name)
  }
};

let person1 = new Person("Lucy");
let person2 = new Person("Joe");

person1.sayName(); //"Lucy"
person1.constructor === Person; // true

person2.sayName(); //"Joe"
person2.constructor === Person; // true

我们重写 prototype 对象时,最好保证 constructor 属性的正确性。

组合模式的优点是该共享的共享了,缺点是封装性不够好。

二、如何理解继承和原型链?

JavaScript 中对象可以通过原型(prototype) 继承其他对象的特征。每个对象都有自己的 prototype 属性,叫做原型。原型和原型链的出现给代码复用提供了解决方案。MDN 的定义如下:

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 __proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

prototype 对象

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。Object() 是 JavaScript 内置的函数。注意 Object() 是函数,不是对象 object,调用 Object() 后返回一个新的对象。

typeof(Object) // "function"

Object()函数有一个匿名的对象,可以通过 prototype 属性访问:

  • Object.prototype 对象上有很多有用的属性和方法,如 toString()
  • Object.prototype 有个重要的属性 constructor,它指向 Object() 函数,Object.prototype.constructor === Object

我们定义一个构造函数:

function Person(name){
	this.name = name;
}

JavaScript 创建了一个新的函数 Person() 并且有匿名的对象 prototype

Object() 函数一样,Person() 函数也有自己的 prototype 指向一个匿名的对象,这个对象也有 constructor 属性指向 Person() 函数。

function Person(name){
	this.name = name;
}

console.log(Person);
console.log(Person.prototype);

打开 Chrome 浏览器的开发者工具,在 Console 中粘贴代码可以看到:

此外,JavaScript 将 Person.prototype 对象和 Object.prototype 对象通过 [[Prototype]] 连接。

原型链

获取原型链

__proto__Object.prototype 对象的可访问属性,它是内部原型链[[prototype]]对象对外暴露的可访问属性。

__proto__

在 ES6 中为了确保浏览器的兼容性,建议使用 Object.getPrototypeOf()替代。

console.log(p1.__proto__ === Person.prototype); // true
console.log(p1.__proto__ === Object.getPrototypeOf(p1)); // true

注意 [[Prototype]]__proto__ prototype 的关系:

遵循 ECMAScript 标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf()Object.setPrototypeOf() 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__。但它不应该与构造函数 func prototype 属性相混淆。被构造函数创建的实例对象的 [[Prototype]] 指向 funcprototype 属性。Object.prototype 属性表示 Object 的原型对象。

原型链

我们可以通过 prototype 对象添加属性和方法:

Person.prototype.greet = function() {
  return "Hi, I'm" + this.name + "!";
}

创建一个新的 Person 实例:

let p1 = new Person("John");

JavaScript 创建了一个新的对象 p1 并将 p1 对象和 Person.prototype 对象通过原型连接起来。

原型链,就是下图中黄字部分的链状结构。

我们可以用下面的代码验证:

function Person(name) {
    this.name = name;
}

let p1 = new Person("John");

p1.__proto__ === Person.prototype; // true (1)
Person.__proto__ === Function.prototype; // true (2)
Function.__proto__ === Function.prototype; // true (3)
Function.prototype.__proto__ === Object.prototype; // true (4)
Object.prototype.__proto__ === null; // true (5)

从上可知:

  • p1 是构造函数 Person 创建的实例,它的内部原型链 [[Prototype]] 对象指向构造函数 Person 的原型对象 prototype。(代码 1 处)
  • 构造函数 Person 的内部原型链 [[Prototype]] 对象指向构造函数 Function 的原型对象 prototype。(代码 2 处)
  • 函数和对象也会一直向上查找,直到 null 为止。(代码 3、 4、 5处)

性能

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。

function Person(name) {
    this.name = name;
}

let p1 = new Person("John");

console.log(p1.hasOwnProperty("name")); // true
console.log(p1.hasOwnProperty("toString")); // false

toString() 方法可以通过原型链查找到,是继承的属性。

总结

  • Object() 函数有一个 prototype 属性,指向 Object.prototype 对象。Object.prototype 对象有对象所有的属性和方法,比如 toString()valueOf()
  • Object.prototype 对象有 constructor 属性,指向 Object 函数。
  • 每个函数都有一个 prototype 对象,这个原型对象的 __proto__ 指向 Object.prototype, 通过 [[prototype]] 或者 __proto__ 属性查找属性和方法。
  • Object.getPrototype() 方法返回给定对象的原型对象,建议使用 Object.getPrototypeOf() 替代 __proto__
  • 通过 hasOwnProperty 检查对象是否具有自己定义的属性。

小练习

下面代码的输出结果是什么?

Function.__proto__ === Object.prototype; // false
Object instanceof Function; // true
Function instanceof Object; // true



阅读量:1678

点赞量:0

收藏量:0