JavaScript 中有八种数据类型,有七种基本数据类型和对象(Object),对象就是引用类型。这里我们介绍八种创建对象的方式:
Object
构造函数
对象字面量
Object.create()
工厂模式
构造函数模式
原型模式
组合模式
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"
类用于创建对象的模版,它建立在原型上。类是“特殊的函数”,类语法有两个组成部分:类表达式和类声明。
我们用类声明来创建一个对象:
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 对象添加更多属性和方法。
它的优缺点如下:
构造函数是用于创建特定类型对象的。像 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
操作符调用构造函数会执行如下操作:
[[Prototype]]
特性被赋值为构造函数的 prototype
属性this
指向新对象)那么 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
自定义构造函数模式的优缺点如下:
这个缺点从上文的例子来看,person1
和 person2
的都有名为 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()
函数,所以 person1
和 person2
共享了定义在全局作用域上的 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()
函数有一个匿名的对象,可以通过 prototyp
e 属性访问:
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]]
指向 func
的 prototype
属性。Object.prototype
属性表示 Object 的原型对象。
我们可以通过 prototype
对象添加属性和方法:
Person.prototype.greet = function() {
return "Hi, I'm" + this.name + "!";
}
创建一个新的 Person
实例:
let p1 = new Person("John");
JavaScript 创建了一个新的对象 p1
并将 p1
对象和 Person.prototyp
e 对象通过原型连接起来。
原型链,就是下图中黄字部分的链状结构。
我们可以用下面的代码验证:
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 处)[[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