面向对象的JavaScript:不仅仅是类
#javascript #oop #关闭 #prototype

我偶然发现了我15年前在法语中写的这篇文章,但我发现它仍然很重要。尽管它很受欢迎,但JavaScript还是一种应有的知名度和认可的语言。因此,我决定将其翻译成英文,并在轻微刷新后重新发布!

本文假设您熟悉JavaScript语法。请注意,我不是讨论eCmascript 6中引入的基于类的对象取向,而是基于原型概念从语言开始中出现的本机对象方向。

所以,让我们深吸一口气,深入了解物体,原型和封闭的神秘世界!

注意
示例中使用的print()函数不是eCmascript标准的一部分。在控制台模式下,它在标准输出上显示文本。在浏览器中,可以用Document.write()alert()替换。在node.js中,您可以使用console.log()

真正的物体碎片

对象

JavaScript从根本上围绕对象的概念构建。其中的所有内容都是对象或对象的引用。例如,数组,类型甚至功能是对象。对象以(名称,值)对的形式包含称为属性的成员,称为属性。属性值可以是字符串,数字,布尔值或其他对象(包括数组和功能)。

让我们从一个简单的示例开始,以表示我的狗雷克斯:

// My dog Rex
var myDog = new Object();
myDog.name = "Rex";
myDog.gender = "Male";
print(myDog.name);
print(myDog.gender);

执行上述代码返回:

Rex
Male

我们还可以声明和操纵像关联数组这样的对象:

// Declaration as an array
var myDog = new Array();
myDog["name"] = "Rex";
myDog["gender"] = "Male";
print(myDog["name"]);
print(myDog["gender"]);
Rex
Male

myDog仍然是一个对象,它的属性可以像数组元素一样访问。

JavaScript对象也有速记声明:

// Shorthand declaration
var myDog = {name: "Rex", gender: "Male"};
print(myDog.name);
print(myDog["gender"]);
Rex
Male

JSON(JavaScript对象表示法)序列化格式已采用了此表示法,该格式允许通过网络交换结构化数据。它通常用AJAX Web应用程序用作XML格式的替代方案,用于异步客户端服务器通信。

现在,让我们给雷克斯说话的能力:

myDog.bark = function() {
  return "Woof!";
}
print(myDog.bark());
Woof!

在JavaScript中,由于函数是对象,因此可以将它们声明为其他对象的属性,从而创建方法。但是,请注意,JavaScript不会区分对象的属性,无论它们是属性还是方法。

构造函数和对象类型

现在,想象我还拥有一只名叫Mirza的母狗,并想代表她的JavaScript。我们可以复制以前的声明,但理想情况下,我们将重复使用Rex和Mirza的共同结构,即创建一个对象类型Dog

JavaScript通过使用称为构造函数的特殊功能,如下:

// Dog object constructor function
function Dog(name, gender) {
  this.name = name;
  this.gender = gender;
  this.bark = function() {
    return "Woof!";
  }
}
// Creating new Dog type objects
var myDog = new Dog("Rex", "Male");
var myFemaleDog = new Dog("Mirza", "Female");
print(myDog.name);
print(myFemaleDog.gender);
Rex
Female

让我们澄清上述示例中使用的两个关键字:

  • new是对象创建操作员。当应用于函数时,该函数用作创建新对象的构造函数。
  • this在构造函数中,指的是正在创建的对象。因此,这个名称对应于由Dog构造函数创建的对象的属性,name参数的值传递给了函数。

尽管JavaScript处理原始类型(可通过typeof操作员检索),但考虑到对象的类型是其构造函数的名称(可通过constructor.name.name属性检索)更有用。根据这个角度,由JavaScript管理的类型如下:

  • 对象:通用,未构图的对象;
var myObject = {firstName: "Joe", lastName: "Black"};
print(myObject.constructor.name);
Object
  • 布尔值:布尔(True或False);
var myBoolean = (1 == (2-1));
print(myBoolean.constructor.name);
Boolean
  • 数字:整数或十进制数字;
var myNumber = 1;
print(myNumber.constructor.name);
Number
  • 字符串:字符串字符串;
var myString = "Hello";
print(myString.constructor.name);
String
  • 数组:数组;
var myArray = [1, "brt"];
print(myArray.constructor.name);
Array
  • 函数:包含代码段的函数;
var myFunction = function() {
  return "hi there";
} 
print(myFunction.constructor.name);
Function
  • [constructorname] :通过构造函数定义的类型。
function Dog(name, gender

) {
  this.name = name;
  this.gender = gender;
  this.bark = function() {
    return "Woof!";
  }
}
var myDog = new Dog("Rex", "Male");
print(myDog.constructor.name);
Dog

顺便说一下,让我们回想起JavaScript是一种动态键入的语言。这意味着它不预先知道变量的类型(或者是变量引用的对象)。它依赖于变量的值来确定其类型,并能够基于上下文转换变量的类型。

例如:

var monNombre = 34;
var maChaine = "5";
print(monNombre + maChaine);
345

封装

尽管JavaScript是一种解释的语言,但它可以通过区分几个可见性来实现封装原则:

  • 公共属性:默认情况下,每个人都可以访问对象的组件(属性或方法)。
function Object1() {
  this.attribute = "Public attribute";
}
Object1.prototype.function = function() {
  return "Public method";
}
var MyObject1 = new Object1();
print(MyObject1.attribute);
print(MyObject1.function());
Public attribute
Public method
  • 私有属性:构造函数中创建的属性仅适用于对象的私有方法。
function Object2() {
  var attribute = "Private attribute";
  function function() {
    return "Private method";
  }
}
var MyObject2 = new Object2();
print(MyObject2.attribute);
try {
  print(MyObject2.function());
} catch(e) {
  print(e);
}
undefined
TypeError: MyObject2.function is not a function
  • 特权方法:可以在外部访问时可以访问对象的私有属性的方法。
function Object3() {
  var attribute = "Private attribute";
  this.function = function() {
    return(attribute);
  };
}
var MyObject3 = new Object3();
print(MyObject3.function());
Private attribute

名称空间

正如我们已经看到的,脚本的默认执行上下文与全局范围关联。因此,在功能之外声明的所有对象和所有参考都是全局属性。由于这可以造成冲突,建议使用对象在命名套件中创建名称空间和隔离变量。

这是如下:

// Package declaration
var org = {}; // Package root
org.test = {}; // Package branch

// Package members
org.test.Dog = function(name, gender) {
  this.name = name;
  this.gender = gender;
};

org.test.Dog.prototype.bark = function() {
  return this.name + " barks!";
};

// Using the package
var myDog = new org.test.Dog("Rex", "Male");
print(myDog.bark());
Rex barks!

原型

在我们的示例中,Dog不是一个班级,而是一个对象,当然是一个特殊的对象,而是一个对象。实际上,JavaScript并未实现基于类的编程模型(这导致一些人说这不是面向对象的语言),而是基于原型的编程模型,类似于自我语言。

对于每个函数,JavaScript将特定对象(称为原型)关联,以作为使用new指令创建的所有对象的参考。该对象可通过构造函数的prototype属性访问。

当值分配给属性时,JavaScript如果尚不存在,则在对象级别上创建属性。另一方面,当请求属性值时,JavaScript执行以下操作:

  • 如果属性存在于对象级别,则其值将返回;否则,JavaScript访问对象的原型并返回原型属性的值;
  • 否则,JavaScript访问原型的原型,依此类推;
  • 如果无法通过上升到该原型链来找到属性,则JavaScript最终到达最高原型,即通用对象的原型,然后返回undefined

由于对象的原型也是对象,因此可以动态修改。修改原型属性(属性或方法)会影响上述机制引起的所有实例。这允许对共享相同原型的所有对象进行动态和追溯修改。

例如:

function Dog(name, gender) {
  this.name = name;
  this.gender = gender;
  this.bark = function() {
    return "Woof!";
  };
}

myDog = new Dog("Rex", "Male");

Dog.prototype.sleep = function() {
  return "Shh! " + this.name + " is sleeping...";
}

print(myDog.bark());
print(myDog.sleep());

myDog.sleep = function() {
  return "Zzz...";
};

print(myDog.sleep());
Woof!
Shh! Rex is sleeping...
Zzz...

让我们逐步分解JavaScript解释器执行的操作,以充分了解对象与其原型之间的关系:

Prototype Chain

  • 步骤1 - 解释器检测一个功能:
    • 创建了代表Dog函数的对象(F)。
    • 如果函数用作构造函数,则对象(这里的P)将用作原型。
  • 步骤2 - 分配到变量myDog
    • 创建了一个空白对象(这里的O)。
    • Dog函数用作构造函数,带有隐式参数this = O
    • 对象O现在具有属性:名称,性别和树皮。
    • 变量myDog是对对象O的引用。
  • 步骤3 - 在构造函数的原型中添加属性:
    • 将树皮属性添加到Object P
  • 步骤4 - 调用可变myDogbark()方法:
    • myDog指向对象O
    • 对象O具有bark属性,因此将其返回(并且由于它是一个函数,已执行并返回其值)。
    • myDog.bark()返回“ woof!”
  • 步骤5 - 调用可变myDogsleep()方法:
    • myDog指向对象O
    • 对象O没有sleep属性,因此javascript转到其原型。
    • 对象的原型O是其构造函数的原型,它是对象P
    • 对象P具有sleep属性,因此将其返回(并且由于它是一个函数,因此已执行并返回其值)。
    • myDog.sleep()返回“嘘!雷克斯正在睡觉...”
  • 步骤6 - 重写可变myDogsleep()属性:
    • myDog指向对象O
    • 创建了对象Osleep属性。
  • 步骤7 - 调用可变myDogsleep()方法:
    • myDog指向对象O
    • 对象O现在具有sleep属性,因此JavaScript不会转到其原型。
    • myDog.sleep()返回“ zzz ...”

通过其原型这种动态修改机制也适用于JavaScript的预定义对象:

String.prototype.reverse = function() {
  var reversed = "";
  for (i = this.length; --i >= 0;) {
    reversed += this.charAt(i);
  }
  return reversed;
} 

var myString = "Hello!";

print(myString.reverse());
!olleH

现在所有字符串都可以使用reverse()方法!

让我们将内容翻译成英文:

静态特性

在构造函数之外声明的属性(因此原型中不存在)未由实例化对象引用。这允许在基于类的编程中实现等效的静态方法和属性(有时称为类方法和属性):

var Dog = function(name, gender) {
  this.name = name;
  this.gender = gender;
  this.bark = function() {
    return "Woof!";
  }
}

//Static method for detecting identical names
Dog.hasSameName = function(dog1, dog2) {
  return (dog1.name == dog2.name);
}

var myDog = new Dog("Rex", "Male");
var myBitch = new Dog("Mirza", "Female");

print(Dog.hasSameName(myDog, myBitch));

myBitch.name = "Rex";
print(Dog.hasSameName(myDog, myBitch));
false
true

遗产

基于原型的继承

正如我们已经指出的那样,基于原型的编程模型不使用类和类实例的概念,而仅使用对象的概念。因此,在静态定义的类之间,而是直接在对象原型之间定义的类之间的继承。

通过将父对象分配给孩子的原型来实现两个对象之间的继承。然后,可以向孩子添加新属性,甚至可以替换父母继承的属性。

现在让我们想象我也想代表我的猫猫,同时重复我们为雷克斯和米尔扎所做的事情。

Inheritance

让我们创建一个父对象动物和两个子对象,狗和猫:

// Parent object
function Animal(name, gender) {
  this.name = name;
  this.gender = gender;
  this.eat = function() {
    return this.name + " eats";
  }
  this.sleep = function() {
    return "Quiet! " + this.name + " is sleeping...";
  }
}

// First child object
function Dog(name, gender) {
  // Passing parameters to the parent object
  Animal.call(this, name, gender);
} // which inherits from Animal

Dog.prototype = new Animal();
// but is of type Dog
Dog.prototype.constructor = Dog;

// Add a method to the child
Dog.prototype.bark = function() {
  return this.name + " barks!";
}

// Second child object
function Cat(name, gender) {
  // Passing parameters to the parent object
  Animal.call(this, name, gender);
} // which inherits from Animal

Cat.prototype = new Animal();
// but is of type Cat
Cat.prototype.constructor = Cat;

// Add a method to the child
Cat.prototype.meow = function() {
  return this.name + " meows!";
}

var myDog = new Dog("Buddy", "Male");
var myCat = new Cat("Whiskers", "Male");

// Add a method to the parent object, after creating child instances
Animal.prototype.eat = function() {
  return this.name + " eats";
}

print(myDog.sleep());
print(myDog.bark());
print(myDog.eat());

print(myCat.sleep());
print(myCat.meow());
print(myCat.eat());
Quiet! Buddy is sleeping...
Buddy barks!
Buddy eats
Quiet! Whiskers is sleeping...
Whiskers meows!
Whiskers eats

注意JavaScript Function对象的call()方法的使用,该方法允许将函数称为属于其应用的对象。我们使用此功能在孩子和父母之间传递名称和性别参数。

多元继承

尽管JavaScript不支持多个继承,但可以使用上述call()方法部分模拟此行为。但是,由于一个对象一次只能具有一个构造函数(因此一个原型),因此无法维护父对象和子对象之间的动态链接。

让我们通过原型继承来代表一个狼人,这既是Man又是Wolf

Multiple inheritance

// First parent object
function Wolf() {
  this.bite = function() {
    return this.name + " has bitten you!";
  };
}

// Second parent object
function Man() {
  this.speak = function() {
    return this.name + " speaks to you...";
  };
}

// Child object
function Werewolf(name) {
  this.name = name;
  Wolf.call(this); // Call the constructor of the first parent
  Man.call(this); // Call the constructor of the second parent
  this.transform = function() {
    return this.name + " has transformed!";
  };
}

var myWerewolf = new Werewolf("Jack");

print(myWerewolf.speak());
print(myWerewolf.transform());
print(myWerewolf.bite());
Jack speaks to you...
Jack has transformed!
Jack has bitten you!

动态继承

使用new操作员创建新对象时,将创建对象分配了对构造函数原型的隐式引用。由于该原型也是一个对象,因此通过修改其原型链动态修改对象的继承似乎是可行的。

现在,让我们尝试代表狼人,这是男人或狼,但从来没有同时:

Dynamic inheritance

// First parent object
function Wolf() {
}

Wolf.prototype.speak = function() {
  return this.name + " growls...";
};

Wolf.prototype.bite = function() {
  return this.name + " has bitten you!";
};

// Second parent object
function Man() {
}

Man.prototype.speak = function() {
  return this.name + " speaks to you...";
};

Man.prototype.bite = function() {
  return this.name + " bit his tongue!";
};

// Child object
function Werewolf(name) {
  this.name = name;
}

Werewolf.prototype = new Man();

var myWerewolf = new Werewolf("Jack");
print(myWerewolf.speak());

Werewolf.prototype = new Wolf();
print(myWerewolf.speak());

var myWerewolf2 = new Werewolf("Joe");
print(myWerewolf2.speak());

遗憾的是,以前的代码返回以下内容:

Jack speaks to you...
Jack speaks to you...
Joe growls...

myWerewolf实例没有受到其构造函数原型的修改的影响。这可以通过以下事实来解释,即即使不是其构造函数的新原型,它的隐式原型属性仍然指向同一对象。另一方面,新的myWerewolf2实例确实具有对新原型的隐式引用。

要动态修改对象的继承,我们需要能够直接修改对构造函数原型的隐式引用。该物业的实施虽然在ECMA-262标准中进行了讨论,但并不是强制性的。因此,它在所有Ecmascript实施中都不可用。
它通常由__proto__属性实施。以下代码证明了__proto__属性确实指向对象的构造函数的原型:

// Parent object
function Wolf() {
}

Wolf.prototype.speak = function() {
  return this.name + " growls...";
};

Wolf.prototype.bite = function() {
  return this.name + " has bitten you!";
};

// Child object
function Werewolf(name) {
  this.name = name;
}

Werewolf.prototype = new Wolf();

var myWerewolf = new Werewolf("Jack");
print((myWerewolf.__proto__ === Werewolf.prototype));
true

因此,我们现在可以动态地重新分配狼人的父母:

// First parent object
function Wolf() {
}

Wolf.prototype.speak = function() {
  return this.name + " growls...";
};

Wolf.prototype.bite = function() {
  return this.name + " has bitten you!";
};

// Second parent object
function Man() {
}

Man.prototype.speak = function() {
  return this.name + " speaks to you...";
};

Man.prototype.bite = function() {
  return this.name + " bit his tongue!";
};

// Child object
function Werewolf(name) {
  this.name = name;
}

Werewolf.prototype = new Man();

// Reassignment of the Werewolf's prototype
Werewolf.prototype.transform = function() {
  switch (this.__proto__.constructor.name) {
    case "Man":
      Wolf.call(this);
      this.__proto__.__proto__ = new Wolf();
      return this.name + " has transformed into a Wolf!";
      break;
    case "Wolf":
      Man.call(this);
      this.__proto__.__proto__ = new Man();
      return this.name + " has transformed into a Man!";
      break;
  }
}

var myWerewolf = new Werewolf("Jack");

print(myWerewolf.speak());
print(myWerewolf.bite());
print(myWerewolf.transform());

print(myWerewolf.speak());
print(myWerewolf.bite());
print(myWerewolf.transform());

print(myWerewolf.speak());
Jack speaks to you...
Jack bit his tongue!
Jack has transformed into a Wolf!
Jack growls...
Jack has bitten you!
Jack has transformed into a Man!
Jack speaks to you...

这个动态继承示例演示了JavaScript的力量。

关闭

封闭是JavaScript的一个非常有力的功能,但它们仍然相对未知或经常被误解,导致无意产生故障代码。简单地说,即使封闭功能完成执行后,它也是内部函数访问封闭功能的属性的能力。

这是通过JavaScript实现的两种机制:执行上下文和范围实现的。使用每个函数调用,JavaScript创建了一个新的执行上下文,并将其与一个范围关联,由一系列对象组成,这些对象确定了函数变量如何初始化。这适用于嵌套在其他功能中的功能,其中包括递归功能。这些对象带有导致函数执行的不同上下文。最低的执行上下文是JavaScript脚本本身的上下文,由Global对象携带。

示例1

从理论意义上讲,封闭的概念并不容易掌握,所以让我们用一个具体的示例来说明它:

01: var number1 = 7;
02: var number2 = -2;
03:
04: function Outer(num) {
05:   var factor = 3;
06:   this.Inner = function (coef) {
07:    return (num * factor * coef);
08:   };
09: }
10:
11: var MyObject1 = new Outer(number1);
12: print(MyObject1.Inner(10));
13:
14: var MyObject2 = new Outer(number2);
15: print(MyObject2.Inner(100));
210
-600

Closure

让我们深入研究JavaScript解析上述代码时所做的事情:

  • 全局上下文

    • 第1和2行:在最基本的执行上下文中,JavaScript添加了属性number1number2,并将值7和-2添加到Global对象,该对象保持脚本的执行上下文。
  • 步骤1

    • 第11行:JavaScript通过执行Outer作为构造函数来创建一个新对象MyObject1
    • 第4行:JavaScript为Outer函数建立了一个新的执行上下文,并且携带num参数和功能属性(包括factor)的对象(在图中称为“外呼叫范围”)。此执行上下文的范围由与Global链接的“外呼叫范围”对象组成。
    • 第6行:JavaScript为Inner函数创建了一个新的执行上下文,并带有coef参数的对象(在图中称为“内呼叫范围”)。此执行上下文的范围由与“外部呼叫范围”链接的“内呼叫范围”对象,然后是Global
    • 第8行:执行Inner函数的结论是结束的,但其范围保留在内存中,因为它是由另一个执行上下文引用的(使用return)。
    • 第9行:执行Outer功能结束,其范围不再引用,并且将在下一个垃圾收集器周期中删除。 JavaScript返回全局执行上下文。
    • 第12行:称为Inner方法,JavaScript使用保留的上下文和范围来初始化变量并执行函数:
    • num等于7;
    • 因子等于3;
    • COEF等于10;
    • inner()返回210。
  • 步骤2

    • 第14行:上一个过程再次执行,导致创建新的执行上下文和新范围,与步骤1中的范围不同。
    • 第15行,在这个新范围中:
    • num等于-2;
    • 因子等于3;
    • COEF等于100;
    • Inner()这次返回-600。

示例2

这是用于实现函数函数的闭合的第二个示例:

function CreateDogAction(action) {
  return function() {
    return this.name + " is " + action;
  }
}

function Dog(name) {
  this.name = name;
  this.eat = CreateDogAction("eating");
  this.sleep = CreateDogAction("sleeping");
  this.bite = CreateDogAction("biting");
}

var myDog = new Dog("Rex");
print(myDog.eat());
print(myDog.sleep());
print(myDog.bite());
Rex is eating
Rex is sleeping
Rex is biting

示例#3

闭合可以包含多个访问公共属性的功能,如此计数器管理示例所示:

function initCounter(start) {
  // Private variable
  var counter = start;

  // Create the counter manipulation functions
  getCounter = function() {
    return counter;
  };

  incCounter = function() {
    counter++;
  };

  setCounter = function(value) {
    counter = value;
  };

  resetCounter = function() {
    counter = start;
  };
}

initCounter(0);
setCounter(10);
incCounter();
print(getCounter());

resetCounter();
print(getCounter());
11
0

结论

本文是为了阐明JavaScript的某些功能,这些功能通常与动态网页功能相关联时通常不会引起人们的注意。但是,更深入地研究JavaScript确实是一种有力的,不断发展的语言,是现代Web应用程序成功的基础。

缺乏有关JavaScript对象特征的知识,可能与基于原型的编程模型不熟悉有关。这可能就是为什么Ecmascript版本6还将基于类的编程模型引入语言的原因。

这真的是明智的举动吗?我会留下来供你决定。我认为,这当然并没有鼓励原型的广泛使用,毕竟,这是JavaScript的核心和力量...