今天看啥  ›  专栏  ›  云影sky

前端面试必备 | 使用原型和构造函数创建对象(原型篇:上)

云影sky  · 掘金  ·  · 2019-12-21 12:33
阅读 140

前端面试必备 | 使用原型和构造函数创建对象(原型篇:上)

本文翻译自 medium.com/better-prog…,作者 Rupesh Mishra,翻译时有删改,标题有改动。

在这篇文章中,我们将会讨论 JavaScript 中的原型是什么,以及在 JavaScript 中如何实现面向对象编程。

使用构造函数 constructor 创建对象的问题

看看下面的代码:

function Human(firstName, lastName) {
  this.firstName = firstName,
  this.lastName = lastName,
  this.fullName = function() {
    return this.firstName + " " + this.lastName;
  }
}
var person1 = new Human("Virat", "Kohli");
console.log(person1)
复制代码

我们使用 Human 构造函数创建 person1person2 两个对象:

var person1 = new Human("Virat", "Kohli");
var person2 = new Human("Sachin", "Tendulkar");
复制代码

当执行上面的代码之后,JavaScript 引擎将会创建两个该构造函数的拷贝,一个是 person1 另一个是 person2

构造函数创建对象

从图中可以看出,每个对象都拥有 firstName lastName fullName 这三个属性。每个使用构造函数创建的对象都会拥有自己的属性和方法,但是我们没有必要声明多个 fullName 函数,这会浪费内存。接下来,我们看看如何解决这个问题。

原型

在 JavaScript 中创建一个函数的时候,JavaScript 引擎会添加一个 prototype 属性给这个函数,这个 prototype 属性指向一个对象,这就是我们所说的原型对象。原型对象默认会有一个 constructor 属性,这个 constructor 指向的就是有 prototype 属性的函数。看下面的图示:

如上图,Human 构造函数有一个 prototype 属性指向原型对象。而原型对象有一个 constructor 属性又指回了这个构造函数。让我们看下面的代码:

function Human(firstName, lastName) {
  this.firstName = firstName,
  this.lastName = lastName,
  this.fullName = function() {
    return this.firstName + " " + this.lastName;
  }
}
var person1 = new Human("Virat", "Kohli");
console.log(person1)
复制代码

输出的结果

通过下面的代码访问 Human 构造函数的原型属性:

console.log(Human.prototype)
复制代码

从上面的图片可以看出,函数的原型属性是一个有两个属性的对象(原型对象),两个属性如下:

  1. constructor 属性,这个属性指向构造函数 Human
  2. __proto__ 属性,后面我们讨论继承的文章中将会讨论这个属性;

使用构造函数创建对象

当一个对象被创建的时候,JavaScript 引擎添加了一个 __proto__ 属性给新创建的对象。__proto__ 属性会指向构造函数的原型对象(构造函数的 prototype 属性指向的对象)。

如上图所示, 使用 Human 构造函数创建的 person1 对象有一个 __proto__ 属性,这个属性指向的就是构造函数的原型对象。

var person1 = new Human("Virat", "Kohli");
复制代码

从上面图片可以看出,person1__proto__ 属性所指向的对象和 Human.prototype 属性所指向的对象相同。我们使用 === 检验一下:

// true
console.log(Human.prototype === person1.__proto__ )
复制代码

上面全等符号得到的结果是 true,所以我们可以进一步确定新对象的 __proto__ 属性指向的对象就是构造函数的原型对象。

接下来我们是使用 Human 构造函数创建 person2 对象:

var person2 = new Human("Sachin", "Tendulkar");
console.log(person2);
复制代码

上面图中可以看出, person2__proto__ 也是指向的 Human.prototype(构造函数的原型对象)。

所以有下面的结果

Human.prototype === person2.__proto__ //true
person1.__proto__ === person2.__proto__ //true
复制代码

所以,person1person2__proto__ 属性都指向 Human 构造函数的原型对象。

构造函数的原型对象会在所有使用该构造函数创建的对象中共享。

原型对象

因为原型对象是对象,所以我们可以添加属性和方法给原型对象。因此允许使用构造函数创建的所有对象共享那些属性和方法。

可以使用点表示法或方括号表示法将新属性添加到构造函数的原型对象中,如下所示:

//Dot notation
Human.prototype.name = "Ashwin";
console.log(Human.prototype.name)//Output: Ashwin

//Square bracket notation
Human.prototype["age"] = 26;
console.log(Human.prototype["age"]); //Output: 26

console.log(Human.prototype);
复制代码

nameage 属性已添加到 Human原型对象中。

例子

//Create an empty constructor function
function Person(){
}
//Add property name, age to the prototype property of the Person constructor function
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;
Person.prototype.sayName = function(){
  console.log(this.name);
}

//Create an object using the Person constructor function
var person1 = new Person();

//Access the name property using the person object
console.log(person1.name)// Output" Ashwin
复制代码

我们分析一下 console.log(person.name) 执行的时候发生了什么。让我们看看 person1 是否有 name 属性。

console.log(person1);
复制代码

正如我们所见, person1 对象是空的,并且它只有一个 __proto__ 属性。那么 console.log(person.name) 的结果是 Ashwin 是如何发生的呢?

当我们尝试访问对象的属性时,JavaScript 引擎首先尝试在对象上查找属性,如果该属性存在于对象上,则它会输出其值。但是,如果该属性不存在于对象上,则它将尝试在 __proto__ 上找到该属性。如果找到了属性,则返回值,否则 JavaScript 引擎将尝试在对象的 __proto__ 上找到该属性。这条查找链一直持续到 __proto__ 属性为 null,在这种情况下,输出将为 undefined

因此,当获取 person1.name 的值时,JavaScript 引擎会检查 person1 对象上是否存在该属性。此时,name 属性不在 person1 对象上。所以 JavaScript 引擎会依据 __proto__ 找到上层对象,并且在该对象中查找 name 属性是否存在。此时,name 属性存在于 person1.__proto__ 中, 所以返回查找到的值 Ashwin

让我们使用 Person 构造函数创建一个对象 person2

var person2 = new Person();
//Access the name property using the person2 object
console.log(person2.name)// Output: Ashwin
复制代码

现在我们在 person1 上定义一个 name 属性:

person1.name = "Anil"
console.log(person1.name)//Output: Anil
console.log(person2.name)//Output: Ashwin
复制代码

这里 person1.name 输出 Anil。如前所述,JavaScript 引擎首先尝试在对象本身上查找属性。在这种情况下,person1 对象本身存在 name 属性,因此 JavaScript 引擎输出person1name 属性值 Anil

而对于 person2,对象上不存在 name 属性。因此,它输出 person2 原型对象上的 name 属性值 Ashwin

原型存在的问题

由于原型对象在使用构造函数创建的所有对象之间共享,因此其属性和方法也在所有对象之间共享。如果对对象 A 具有原始值的原型属性进行修改,则其他对象将不会受到影响,这将在其对象上创建一个属性,如下所示。

console.log(person1.name);//Output: Ashwin
console.log(person2.name);//Output: Ashwin

person1.name = "Ganguly"

console.log(perosn1.name);//Output: Ganguly
console.log(person2.name);//Output: Ashwin
复制代码

上面代码前两行,获取 name 属性都是在原型对象上获取的,而 person1.name = "Ganguly" 则直接给 person1 对象添加了 name 属性,所以下面打印的时候,是直接从 person1 对象上获取到的 name 属性的值,而 person2 由于没有这个属性,所以依然会到原型对象上获取 name 属性。

让我们来看另外一个原型的例子,在这个例子中原型对象有一个引用类型的属性。更改这个引用数据将会出现问题。

//Create an empty constructor function
function Person(){
}
//Add property name, age to the prototype property of the Person constructor function
Person.prototype.name = "Ashwin" ;
Person.prototype.age = 26;

Person.prototype.friends = ['Jadeja', 'Vijay'],//Arrays are of reference type in JavaScript
Person.prototype.sayName = function(){
  console.log(this.name);
}

//Create objects using the Person constructor function
var person1= new Person();
var person2 = new Person();

//Add a new element to the friends array
person1.friends.push("Amit");

console.log(person1.friends);// Output: "Jadeja, Vijay, Amit"
console.log(person2.friends);// Output: "Jadeja, Vijay, Amit"
复制代码

在上面的例子中, person1person2 都指向原型对象中 friends 属性对应的数组。 person1 在这个数组中添加了一个字符串。

由于 friends 数组是存在于 Person.prototype (原型对象)上的,所以它不属于 person1,因此 person1 更改 friends 数组时,更改的是原型对象的数组。所以 person2.friends 访问的同样也是这个被更改过的数组。

上面的操作如果是为了让所有的实例都共享这个被变更过的数组,那这样做是没问题的。但是明显,这里我们并不想要它这样。我们想要 person2 访问到的数组内的数据依然是最开始的那几个,并不想让数组被改变。

组合使用构造函数和原型

我们知道,使用构造函数或者原型创建对象都会存在问题,接下来我们组合使用这两者来解决上面的问题。

  1. 构造函数的问题:每个对象都会声明对应的函数,浪费内存;
  2. 原型的问题:更改引用类型的原型属性的值会影响到其他实例访问该属性;

为了解决上面的问题,我们可以用把所有对象相关的属性定义在构造函数内,把所有共享属性和方法定义在原型上

//Define the object specific properties inside the constructor
function Human(name, age){
  this.name = name,
  this.age = age,
  this.friends = ["Jadeja", "Vijay"]
}
//Define the shared properties and methods using the prototype
Human.prototype.sayName = function(){
  console.log(this.name);
}
//Create two objects using the Human constructor function
var person1 = new Human("Virat", 31);
var person2 = new Human("Sachin", 40);

//Lets check if person1 and person2 have points to the same instance of the sayName function
console.log(person1.sayName === person2.sayName) // true

//Let's modify friends property and check
person1.friends.push("Amit");

console.log(person1.friends)// Output: "Jadeja, Vijay, Amit"
console.log(person2.friends)//Output: "Jadeja, Vijay"
复制代码

我们想要每个实例对象都拥有 name agefriends 属性,所以我们使用 this 把这些属性定义在构造函数内。另外,由于 sayName 是定义在原型对象上的,所以这个函数会在所有实例间共享。

在上面的例子中,person1 对象更改 friends 属性时, person2 对象的 friends 属性没有更改。这是因为 person1 对象更改的是自己的 friends 属性,不会影响到 person2 内的。

最后

往期精彩:

关注公众号可以看更多哦。

感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。

公众号




原文地址:访问原文地址
快照地址: 访问文章快照