当今 JavaScript 大行其道,各种应用对其依赖日深。web 程序员已逐渐习惯使用各种优秀的 JavaScript 框架快速开发 Web 应用,从而忽略了对原生 JavaScript 的学习和深入理解。所以,经常出现的情况是,很多做了多年 JS 开发的程序员对闭包、函数式编程、原型总是说不清道不明,即使使用了框架,其代码组织也非常糟糕。这都是对原生 JavaScript 语言特性理解不够的表现。要掌握好 JavaScript,首先一点是必须摒弃一些其他高级语言如 Java、C# 等类式面向对象思维的干扰,全面地从函数式语言的角度理解 JavaScript 原型式面向对象的特点。把握好这一点之后,才有可能进一步使用好这门语言。本文适合群体:使用过 JS 框架但对 JS 语言本质缺乏理解的程序员,具有 Java、C++ 等语言开发经验,准备学习并使用 JavaScript 的程序员,以及一直对 JavaScript 是否面向对象模棱两可,但希望知道真相的 JS 爱好者。
重新认识面向对象
为了说明 JavaScript 是一门彻底的面向对象的语言,首先有必要从面向对象的概念着手 , 探讨一下面向对象中的几个概念:
一切事物皆对象
对象具有封装和继承特性
对象与对象之间使用消息通信,各自存在信息隐藏
以这三点做为依据,C++ 是半面向对象半面向过程语言,因为,虽然他实现了类的封装、继承和多态,但存在非对象性质的全局函数和变量。Java、C# 是完全的面向对象语言,它们通过类的形式组织函数和变量,使之不能脱离对象存在。但这里函数本身是一个过程,只是依附在某个类上。
然而,面向对象仅仅是一个概念或者编程思想而已,它不应该依赖于某个语言存在。比如 Java 采用面向对象思想构造其语言,它实现了类、继承、派生、多态、接口等机制。但是这些机制,只是实现面向对象编程的一种手段,而非必须。换言之,一门语言可以根据其自身特性选择合适的方式来实现面向对象。所以,由于大多数程序员首先学习或者使用的是类似 Java、C++ 等高级编译型语言(Java 虽然是半编译半解释,但一般做为编译型来讲解),因而先入为主地接受了“类”这个面向对象实现方式,从而在学习脚本语言的时候,习惯性地用类式面向对象语言中的概念来判断该语言是否是面向对象语言,或者是否具备面向对象特性。这也是阻碍程序员深入学习并掌握 JavaScript 的重要原因之一。
实际上,JavaScript 语言是通过一种叫做 原型(prototype)的方式来实现面向对象编程的。下面就来讨论 基于类的(class-based)面向对象和 基于原型的 (prototype-based) 面向对象这两种方式在构造客观世界的方式上的差别。
基于类的面向对象和基于原型的面向对象方式比较
在基于类的面向对象方式中,对象(object)依靠 类(class)来产生。而在基于原型的面向对象方式中,对象(object)则是依靠 构造器(constructor)利用 原型(prototype)构造出来的。举个客观世界的例子来说明二种方式认知的差异。例如工厂造一辆车,一方面,工人必须参照一张工程图纸,设计规定这辆车应该如何制造。这里的工程图纸就好比是语言中的 类 (class),而车就是按照这个 类(class)制造出来的;另一方面,工人和机器 ( 相当于 constructor) 利用各种零部件如发动机,轮胎,方向盘 ( 相当于 prototype 的各个属性 ) 将汽车构造出来。
事实上关于这两种方式谁更为彻底地表达了面向对象的思想,目前尚有争论。但笔者认为原型式面向对象是一种更为彻底的面向对象方式,理由如下:
首先,客观世界中的对象的产生都是其它实物对象构造的结果,而抽象的“图纸”是不能产生“汽车”的,也就是说,类是一个抽象概念而并非实体,而对象的产生是一个实体的产生;
其次,按照一切事物皆对象这个最基本的面向对象的法则来看,类 (class) 本身并不是一个对象,然而原型方式中的构造器 (constructor) 和原型 (prototype) 本身也是其他对象通过原型方式构造出来的对象。
Again, in a class-based object-oriented language, the state of an object is held by the object instance, and the behavior method of the object is held by the class that declares the object, and only the object The structures and methods can be inherited; in prototype object-oriented languages, the behavior and status of the object belong to the object itself and can be inherited together (Reference Resources), which is also closer to objective reality.
Finally, class-based object-oriented languages such as Java, in order to make up for the inconvenience of not being able to use global functions and variables in procedural languages, allow static properties and static methods to be declared in classes. In fact, there is no so-called static concept in the objective world, because everything is an object! In a prototype object-oriented language, except for built-in objects, global objects, methods or properties are not allowed to exist, and there is no static concept. All language elements (primitives) must depend on objects for their existence. However, due to the characteristics of functional languages, the objects on which language elements depend change with changes in the runtime context, which is specifically reflected in changes in the this pointer. It is this characteristic that is closer to the natural view that "everything belongs to something, and the universe is the foundation for the survival of all things." In Listing 1, window is similar to the concept of the universe.
Listing 1. Contextual dependencies of objects
After accepting the fact that there is a method of object-oriented implementation called prototype-based implementation, we can take a deeper look at how ECMAScript constructs its own language based on this method.
The most basic object-oriented
ECMAScript is a complete object-oriented programming language (Reference Resources), of which JavaScript is a variant. It provides 6 basic data types, namely Boolean, Number, String, Null, Undefined, and Object. In order to achieve object-orientation, ECMAScript designed a very successful data structure - JSON (JavaScript Object Notation). This classic structure can be separated from the language and become a widely used data interaction format (Reference Resources).
It should be said that ECMAScript with basic data types and JSON construction syntax can basically implement object-oriented programming. Developers can freely use literal notation (literal notation) to construct an object and directly assign values to its non-existing properties, or use delete Delete attributes (Note: The delete keyword in JS is used to delete object attributes, and is often mistaken for delete in C, which is used to release objects that are no longer used), such as Program List 2 .
Listing 2. Literal notation object declaration
In the actual development process, most beginners or developers who do not have high requirements for JS applications basically only use this part of the ECMAScript definition to meet basic development needs. However, such code reusability is very weak. Compared with other class-based object-oriented strongly typed languages that implement inheritance, derivation, polymorphism, etc., it seems a bit dry and cannot meet the needs of complex JS application development. So ECMAScript introduced prototypes to solve the object inheritance problem.
Use function constructor to construct objects
In addition to the literal notation (literal notation) method, ECMAScript allows the creation of objects through the constructor (constructor) . Each constructor is actually a function (function) object , which contains a "prototype" attribute for implementing prototype-based Prototype-based inheritance and shared properties . Objects can be created by "new keyword constructor call", such as Program List 3: Listing 3. Create objects using constructors
Copy codeThe code is as follows:
In ECMAScript, each object created by a constructor has an
implicit reference (implicit reference)
that points to the value of the constructor's prototype attribute. This reference is calledprototype (prototype). Furthermore, each prototype can have an implicit reference that points to its own prototype (that is, the prototype of the prototype). If this continues, this is the so-called prototype chain (prototype chain) (Reference Resources). In the specific language implementation, each object has a __proto__ attribute to implement the implicit reference to the prototype. Listing 4 illustrates this point. Listing 4. Object’s __proto__ attributes and implicit references
Copy codeThe code is as follows:
// The prototype itself is an Object object, so its implicit reference points to
// The prototype attribute of the Object constructor, so true is printed
console.log( Person.prototype.__proto__ === Object.prototype );
// The constructor Person itself is a function object, so true is printed here
console.log( Person.__proto__ === Function.prototype );
With the prototype chain, you can define a so-called property hiding mechanism and achieve inheritance through this mechanism. ECMAScript stipulates that when assigning a value to an attribute of an object, the interpreter will search for the first object containing the attribute in the object's prototype chain (note: the prototype itself is an object, then the prototype chain is a chain of a set of objects . The first object in the object's prototype chain is the object itself) for assignment. On the contrary, if you want to get the value of an object property, the interpreter naturally returns the object property value that first has the property in the object's prototype chain. Figure 1explains the hidden mechanism:
Figure 1. Property hiding mechanism in the prototype chain
In Figure 1, object1->prototype1->prototype2 constitutes the prototype chain of object object1. According to the above attribute hiding mechanism, you can clearly see that the property4 attribute in the prototype1 object and the property3 attribute in the prototype2 object are both is hidden. Once you understand the prototype chain, it will be very easy to understand the implementation principle of prototype-based inheritance in JS. Program List 5 is a simple example of using the prototype chain to implement inheritance.
List 5. Implement inheritance using the prototype chain Horse->Mammal->Animal
// Declare Horse object constructor
function Horse( height, weight) {
this.name = "horse";
this.height = height;
this.weight = weight;
}
// Specify the prototype of the Horse object as a Mammal object and continue to build the prototype chain between Horse and Mammal
Horse.prototype = new Mammal();
// Re-specify the eat method, this method will override the eat method inherited from the Animal prototype
Horse.prototype.eat = function() {
alert( "Horse is eating grass!" );
}
// Verify and understand the prototype chain
var horse = new Horse( 100, 300 );
console.log( horse.__proto__ === Horse.prototype );
console. log( Horse.prototype.__proto__ === Mammal.prototype );
console.log( Mammal.prototype.__proto__ === Animal.prototype );
The key to understanding the implementation of the object prototype inheritance logic in Listing 5 lies in the two lines of code Horse.prototype = new Mammal() and Mammal.prototype = new Animal(). First, the result on the right side of the equation is to construct a temporary object, and then assign this object to the prototype property of the object on the left side of the equation. That is to say, the newly created object on the right is used as the prototype of the object on the left. Readers can substitute these two equations into the corresponding equations in the last two lines of code in Listing 5 to understand for themselves.
How to implement JavaScript class inheritance
As can be seen from code listing 5, although the prototype-based inheritance method achieves code reuse, its writing is loose and not fluent enough, and its readability is poor, which is not conducive to expansion and effective organization and management of source code. It has to be admitted that class inheritance is more robust in language implementation and has obvious advantages in building reusable code and organizing programs. This has led programmers to look for a way to code in a class-based inheritance style in JavaScript. From an abstract point of view, since class inheritance and prototypal inheritance are both designed to implement object-oriented, and the carrier languages implemented by them are equivalent in terms of computing power (because the computing power of Turing machine and Lambda calculus Computing power is equivalent), then can we find a transformation that enables the prototypal inheritance language to implement a class-based inheritance coding style through this transformation?
At present, some mainstream JS frameworks provide this conversion mechanism, that is, class declaration methods, such as Dojo.declare(), Ext.entend(), etc. Using these frameworks, users can easily and friendly organize their JS code. In fact, before the emergence of many frameworks, JavaScript master Douglas Crockford was the first to use three functions to extend the Function object to achieve this transformation. For details on its implementation, you can (Reference Resources ). There is also the famous Base.js implemented by Dean Edwards (Reference Resources). It is worth mentioning that the father of jQuery John Resig implemented his own Simple Inheritance in less than 30 lines of code after leveraging the strengths of others. It is very simple to declare a class using the extend method it provides. Program Listing 6 is an example of using the Simple Inheritance library implementation class declaration. The last print output statement is the best explanation of Simple Inheritance to implement class inheritance.
Listing 6. Implementing class inheritance using Simple Inheritance
If you already have a good understanding of prototypes, function constructors, closures, and context-based this, it is not too difficult to understand how Simple Inheritance works. Essentially, in the statement var Person = Class.extend(...), the Person on the left actually obtains a constructor returned by Class calling the extend method, that is, a reference to a function object. Following this idea, we continue to introduce how Simple Inheritance does this, and then realizes the conversion from prototype inheritance to class inheritance. Figure 2 is the source code of Simple Inheritance and its accompanying comments. In order to facilitate understanding, the code is explained line by line in Chinese.
Figure 2. Simple Inheritance source code analysis
Put aside the second part of the code and examine the first and third parts coherently as a whole. You will find that the extend function The fundamental purpose is to construct a new constructor with new prototype properties. We can’t help but admire the masterful handwriting of John Resig and his delicate grasp of the essence of the JS language. As for how John Resig came up with such an exquisite implementation method, interested readers can read this article (Reference Resources), which details the thinking process of the initial design of Simple Inheritance.
JavaScript private member implementation
So far, if you are still skeptical about object-oriented JavaScript, then the suspicion must be that JavaScript does not implement information hiding in object-oriented, that is, private and public. Unlike other class-based object-oriented methods that explicitly declare private and public members, JavaScript's information hiding is achieved through closures. See Listing 7:
Listing 7. Using closures to implement information hiding
JavaScript must rely on closures to achieve information hiding, which is determined by its functional language characteristics. This article will not discuss the topics of functional languages and closures, as the above assumes that you understand context-based this in JavaScript. Regarding information hiding in JavaScript, Douglas Crockford has a more authoritative and detailed introduction in the article "Private members in JavaScript" (Reference Resources).
JavaScript is considered to be the most misunderstood programming language in the world, because it wears the cloak of the C language family, but it expresses LISP-style functional language features; it has no classes, but it also fully implements object-oriented. To have a thorough understanding of this language, you must take off its C language coat, return to the perspective of functional programming, and at the same time abandon the object-oriented concept of the original class to learn and understand it. With the popularity of Web applications and the rapid development of the JS language itself in recent years, especially the emergence of back-end JS engines (such as NodeJS based on V8, etc.), it is foreseeable that JS, which was originally just a toy for writing page effects, will gain broader development. world. This development trend also puts forward higher requirements for JS programmers. Only by thoroughly understanding this language can it be possible to use its power in large-scale JS projects.