JavaScript设计模式:构建模型(一)

2021-02-11  本文已影响0人  魂斗驴

学习JavaScript的最困难方面之一就是让您了解对象和所有相关的术语。原型,构造函数,构造属性,dunder原型,原型链等。当您尝试太快时,它很容易变得不知所措。

本文将带您一步一步了解JavaScript的面向对象方法,以便您可以缓慢而逐步地构建心智模型。它是为那些刚接触JavaScript对象而又在与相关概念和术语苦苦挣扎的人们写的。

建立模型

我们将从可能很熟悉的图片开始我们的模型。在考虑与继承相关的类比之前,您可能已经拥有它或等效的产品。

在这里,我们有一个Terrier从类继承的Dog类,而该类又从一个类继承Animal.现在,忘记有关基于类的面向对象编程的所有知识。这是不同的。我们没有类,只有对象。每个对象都链接到另一个对象。因此,我们可以重画上图来表示此“链接”概念。

在这里,terrier对象链接到dog对象,而对象又链接到animal对象。但是它们如何联系?JavaScript中的每个对象都有一个内部[[prototype]]属性,可以直接由proto属性访问。此属性称为“ dunder proto”,本质上指向另一个对象。可以将其视为到另一个对象的链接。考虑到这一点,让我们重新绘制图表!

好的,太好了。我们可以看到该proto属性指向另一个对象。但这对我们意味着什么?

原型链

原型链或原型继承是JavaScript如何从其他对象“继承”属性。如果我们尝试访问某个对象上的属性,而不是该对象直接拥有的属性,则下一个调用端口是该属性指向的对象proto。我们可以使用创建一个链接到另一个对象的对象Object.create(obj)。这将创建一个新对象,该对象的proto属性设置为指向obj,该对象作为参数传入。我们来看一些代码。

var animal = {
  type: 'mammal',
  breathe: function() { 
    console.log("I'm breathing");
  },
}

var dog = Object.create(animal);

console.log(dog);                      // {}
console.log(dog.type);                 // "mammal"
console.log(dog.__proto__);            // { type: 'mammal', breathe: ƒ }
console.log(dog.__proto__ === animal); // true

这样我们就可以清楚地看到dog和之间的关系animal。即使没有直接定义的属性dog,我们仍然可以访问type。让我们terrier从创建第三个对象dog。请注意,我们的terrier对象还将继承该animal对象上定义的属性。
注意:从此处开始的代码块可能涉及很多重复。重复的代码已被注释掉,以帮助您专注于更改/添加。

// var animal = {
//   type: 'mammal',
//   breathe: function() { 
//     console.log("I'm breathing");
//   },
// }

// var dog = Object.create(animal);
var terrier = Object.create(dog);

console.log(terrier.type);                 // "mammal"
console.log(terrier.__proto__)             // {}
console.log(terrier.__proto__ === dog);    // true
console.log(terrier.__proto__ === animal); // false

从上面我们可以看出,这terrier.proto显然是指向dog且dog单独指向的。有两种方法可以让我们查询一个对象与另一个对象之间的原型关系。
第一个Object.prototype.isPrototypeOf()用于检查一个对象是否在另一个对象的原型链上的任何位置。所以对于我们的三个示例对象,我们可以从图中看到的上面animal是两个的原型链dog和terrier,同时dog是对的原型链terrier。让我们测试一下这个假设...

// var animal = {
//   type: 'mammal',
//   breathe: function() { 
//     console.log("I'm breathing");
//   },
// }

// var dog = Object.create(animal);
// var terrier = Object.create(dog);

console.log(animal.isPrototypeOf(terrier)); // true
console.log(animal.isPrototypeOf(dog));     // true
console.log(dog.isPrototypeOf(terrier));    // true

好吧,加起来。但是,如果我们要查找是否dog.proto直接链接到animal而不是位于其原型链上,该怎么办。好吧,我们可以使用dog.proto === animal,在这种情况下,它将返回true。proto但是,此属性直到最近才被标准化,由于某些潜在的问题行为,实际上已被列为已弃用。还有一种查询此关系的方法,即使用Object.getPrototypeOf(obj)。

// var animal = {
//   type: 'mammal',
//   breathe: function() { 
//     console.log("I'm breathing");
//   },
// }

// var dog = Object.create(animal);
// var terrier = Object.create(dog);

console.log(Object.getPrototypeOf(terrier) === dog)    // true
console.log(Object.getPrototypeOf(terrier) === animal) // false

那么,如果dog在创建后向其中添加属性,会发生什么terrier?可以使用此属性terrier吗?

// var animal = {
//   type: 'mammal',
//   breathe: function() { 
//     console.log("I'm breathing");
//   },
// }

// var dog = Object.create(animal);
// var terrier = Object.create(dog);

dog.speak = function() { 
  console.log("Woof Woof"); 
};

terrier.speak(); // "Woof Woof"

创建对象后,是否将属性添加到对象的原型链并不重要。这里terrier.proto实际上指向dog,而不是的副本dog。原型链向上的任何更改都将进一步向下反映。

链接到其他对象的对象(OLOO)

链接到其他对象的对象(OLOO)是一种JavaScript设计模式,可让我们定义一个父对象,从中可以创建其他对象。所有共享属性都将在此父对象上定义。JavaScript中使用一种约定,该父对象的首字母将大写。就是这样,一个约定。归根结底,这只是另一个对象。让我们重新定义我们的思维模型以反映这一约定。


现在假设我们要从创建两个不同的对象Animal。我们将创建一个reptile对象和一个mammal对象。以下是我们要实现的目标的概述:

我们希望这些对象共享一个公共breathe属性,但是具有不同的type属性。一个将是.type = "mammal",另一个将是.type = "reptile”。让我们在心理模型中包括属性名称,以更好地了解我们正在尝试做的事情。

现在让我们看一下代码…

var Animal = {
  init: function(type) { 
    this.type = type;
  },

  breathe: function() {
    console.log("I'm breathing");
  },
}

var Dog = Object.create(Animal);
var Terrier = Object.create(Dog);

var mammal = Object.create(Animal);
mammal.init("mammal");
var reptile = Object.create(Animal);
reptile.init("reptile");

console.log(mammal.type);   // "mammal"
mammal.breathe();           // "I'm breathing"
console.log(reptile.type);  // "reptile"
reptile.breathe();          // "I'm breathing"

您会看到我们init在上定义了一个新方法Animal。此方法用于初始化我们新创建的对象中的值。因此,line 14我们创建一个新对象mammal。由于mammal是从创建的Animal,因此它的proto对象将指向Animal。反过来,这意味着它将有权访问在上定义的属性Animal,包括init方法。此方法没有什么特别的,可以随意调用,但要使用约定init。

如果我们遵循的方法调用上line 15到line 3,需要注意的重要一点是,this将是该方法被调用,在这种情况下对象mammal。因此line 3将等同于mammal.type = "mammal","mammal"作为传入的参数。类似地,在line 17执行时,我们可以遵循line 3,即等同于reptile.type = "reptile"。

因为我们的init方法定义的Animal,它现在也算是提供给Dog和Terrier由于原型继承。因此,如果我们从创建一个新对象Terrier,rex例如,rex也将使用此init方法。快速的视觉效果,然后我们将通过代码看到它。

// var Animal = {
//   init: function(type) { 
//     this.type = type;
//   },

//   breathe: function() {
//     console.log("I'm breathing");
//   },
// }

// var Dog = Object.create(Animal);
// var Terrier = Object.create(Dog);

var rex = Object.create(Terrier);
rex.init("canine");

console.log(rex.type);   // "canine"
rex.breathe();           // "I'm breathing"

JavaScript的OLOO设计模式的基本要素就在这里。这种设计模式可能是最简单的设计模式,并且由于其原型继承而包含了JavaScript。

还有另一种流行的设计模式,即伪古典模式,它使用函数来创建对象。新开发人员可能很难理解这种方法的细微差别。查看图表和心理模型可能看起来像蜘蛛网。因此,我们将逐步建立一个思维模型,每次一步。在继续前进之前,您必须完全掌握心智模型和代码段中的每一项内容,否则您很快就会发现更多的补充内容很重要。

伪古典模式

让我们保持简单,从基于类的继承的基本思维模型开始。

这是伪古典模式尝试模仿的方法。它是通过使用函数而不是类来创建对象的。从下面的工厂函数中我们可以看出,从函数创建对象是多么容易。

function createAnimal() {
  return {
    type: "mammal",

    breathe: function() {
      console.log("I'm breathing");
    }
  }
}

var animal = createAnimal();

console.log(animal.type);                        // "mammal"
animal.breathe();                                // "I'm breathing"
console.log(Object.getOwnPropertyNames(animal)); // ["type", "breathe"]

这种工厂功能方法有两个主要缺点。首先,我们无法识别创建特定对象的函数。在OLOO方法中,我们可以做到Object.getPrototypeOf(rex) === Terrier。在基于类的语言中,我们可以轻松地查询对象以找出其类。我们需要一种方法来识别哪些函数创建了我们的JavaScript对象。

第二个缺点归结为系统资源的使用。在上面的代码片段中,我们可以看到,返回的每个对象createAnimal都有其自己的breathe()方法。对于上面的小片段来说,这一切都很好,但是如果您返回的对象有数百种方法,请想象一下。此外,您的函数可用于创建数百个此类对象。将每种方法复制到每个对象都浪费了很多资源。在OLOO方法中,我们有一个指向包含共享方法的父对象的链接。有一个类似的方法会很好。

参考

JavaScript Design Patterns: Building a Mental Model

上一篇下一篇

猜你喜欢

热点阅读