面向对象与原型

理解原型

在JavaScript中,对象是属性名与属性值的集合。对象属性可以使简单值(如数字、字符串)、函数或其他对象。

  1. let obj = {
  2. prop1:1, // 简单值赋值
  3. prop2:function(){}, // 函数赋值
  4. prop3:{} // 对象赋值
  5. }

同时,JavaScript是动态语言,可以修改或删除对象的属性,也可以为对象添加新属性。

obj.prop1 = 1;
obj.prop1 = [];
delete obj.prop2;
obj.prop4 = "Hello";

在软件开发的过程中,为了避免重复造轮子,我们希望可以尽可能地复用代码。继承是代码复用的一种方式,继承有助于合理地组织程序代码,将一个对象的属性扩展到另一个对象上。在JavaScript中,可通过原型实现继承。
每个对象都含有原型的引用,当查找属性时,若对象本身不具有该属性,则会查找原型上是否有该属性。

const test1 = { a : 1 };
const test2 = { b : 2 };

console.log("b" in test1);     // false

Object.setPrototypeOf(test1,test2);  // 将test2设置为test1的原型
console.log("b" in test1);    // true

在JavaScript中,对象的原型属性是内置属性(使用标记[[protype]]),无法直接访问。相反,内置的方法Object.setPrototypeOf需要传入两个对象作为参数,并将第二个对象设置为第一个对象的原型。
因此,当我们查询test1没有的属性时,test1将查找过程委托给test2底线,通过test1访问test2的属性b。

每个对象都可以有一个原型,每个对象的原型也可以拥有一个原型,以此类推,就形成一个原型链。查找特定属性将会被委托在整个原型链上,只有当没有更多的原型可以进行查找时,才会停止查找。

对象构造器与原型

创建一个新对象的最简单的方法,就是使用如下的语句:

const warrior = {};

// 我们可以通过赋值语句添加属性
warrior.name = 'Saito';
warrior.occupation = 'marksman';

但是那些具有面向对象开发语言背景的人,可能会想念封装和构建类的构造函数。
构造函数是用来初始化对象为已知的初始状态。毕竟如果我们要创建多个相同类型的对象的实例,为每个对象单独进行属性分配,不仅繁琐,而且非常容易出错。
JavaScript提供了这种机制,但与大多数语言有所不同。像面向对象的语言,如Java和C++,JavaScript使用new操作符,通过构造函数初始化新对象,但是没有真正的类定义。通过操作符new,应用于构造函数之前,触发创造一个新对象分配。
每个函数都有一个原型对象,该原型对象指向创建对象的函数。

function Ninja(){};           // 定义一个空函数
Ninja.prototype.swingSword = function(){     // 每个函数都具有可置的原型对象,我们可以对其只有更改
  return true; 
}

const ninja1 = Ninja();       // 作为函数调用Ninja,该函数没有任何返回值
console.log(ninja1);          // undefined 

const ninja2 = new Ninja();        // 作为构造函数调用Ninja,不仅创建了新的实例,并且该实例上具有原型上的方法
console.log(ninja2 && ninja2.swingSword && ninja2.swingSword());  // true

截屏2020-09-25 上午10.30.22.png

  • 每一个函数都具有一个原型对象。
  • 每一个函数的原型都具有一个constructor属性,该属性指向函数本身。
  • constructor对象的原型设置为新创建的对象的原型。

    实例属性

    当把函数作为构造函数,通过操作符new进行调用时,它的上下文被定义为新的对象实例。通过原型暴露属性,通过构造函数的参数进行初始化。 ```javascript function Ninja(){ this.swung = false; // 创建布尔类型的实例变量,并初始化该变量的默认值为false this.swingSword = function(){ // 创建实例方法,该方法的返回值为实例变量swung取反 return !this.swung; } }

Ninja.prototype.swingSword = function(){ // 定义一个与实例方法同名的原型方法 return this.swung; }

const ninja = new Ninja(); console.log(ninja.swingSword()); // true

测试证明,**实例会隐藏原型中与实例方法重名的方法。**<br />在构造函数内部,关键字this指向新创建的对象,所以在构造器内添加的属性直接在新的ninja实例上。然后,当通过ninja范围SwingSword属性时,就不需要遍历原型链,就立即可以找到并返回了在构造器内创建的属性。<br />![截屏2020-09-25 上午10.46.41.png](https://cdn.nlark.com/yuque/0/2020/png/1561137/1601002004122-03c32af6-0b40-4bc0-8473-37ebe2c4b332.png#align=left&display=inline&height=466&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2020-09-25%20%E4%B8%8A%E5%8D%8810.46.41.png&originHeight=466&originWidth=844&size=88833&status=done&style=none&width=844)
<a name="a0uMg"></a>
### JavaScript动态特性的副作用
JavaScript是一门动态语言,可以很容易地添加、删除和修改属性。这种特性同样适合与原型,包括函数原型和对象原型。
```javascript
function Ninja(){       // 定义了一个构造函数,该构造函数中创建了一个swung属性,初始化为布尔值
  this.swung = true;
}

const ninja1 = new Ninja();    // 通过new操作符调用构造函数,创建实例Ninja

Ninja.prototype.swingSword = function(){    // 在实例对象创建完成后,在原型添加一个方法
  return this.swung;
}

console.log(ninja1.swingSword());   // true

Ninja.prototype = {       // 使用字面量对象完全重写Ninja的原型对象,仅有一个pierce方法
  pierce:function(){
    return true;
  }
}

console.log(ninja1.swingSword());   // true  尽管我们完全替换了Ninja的构造器原型,但是实例化的Ninja对象仍然具有swingSword方法,因为对象ninjia1仍然保持着对旧的Ninja原型的引用

const ninja2 = new Ninja();    // 新创建的ninja2实例拥有新原型的引用,因此不具有swingSword方法,仅有pierce方法
console.log(ninja2.pierce());   // true
console.log(ninja2.swingSword);  // undifined