이전에 객체지향에 대해 배운 모든 것을 일시적으로 잊어버리세요. 여기서는 경주 상황만 고려하면 됩니다. 응, 경주 중이야.
최근 프랑스에서 인기 있는 행사인 르망 24시간을 시청하고 있었습니다. 가장 빠른 자동차는 르망 프로토타입으로 알려졌습니다. 이러한 자동차는 "아우디"나 "푸조"와 같은 제조업체에서 제작하지만 거리나 고속도로에서 볼 수 있는 종류의 자동차는 아닙니다. 이 제품은 고속 지구력 이벤트에서 경쟁하기 위해 특별히 제작되었습니다.
제조업체는 이러한 프로토타입 차량의 연구, 개발, 설계 및 제조에 엄청난 돈을 투자하고 엔지니어는 이 프로젝트를 완벽하게 만들기 위해 항상 열심히 노력합니다. 그들은 합금, 바이오 연료, 제동 기술, 타이어 컴파운드 구성 및 안전 기능에 대한 다양한 실험을 수행했습니다. 시간이 지남에 따라 이러한 실험에서 얻은 기술 중 일부는 개선되어 주류 차량 라인에 적용되었습니다. 귀하가 운전하고 있는 자동차의 기술 중 일부가 레이싱 프로토타입에서 처음으로 선보였을 가능성이 있습니다.
이러한 주류 차량은 레이싱 프로토타입 으로부터 기술을 계승했다고 할 수도 있습니다.
이제 JavaScript의 프로토타입 및 상속 문제를 논의할 수 있는 기반이 생겼습니다. C, Java 또는 C#에서 알고 있는 고전적인 상속 패턴과 완전히 같지는 않지만 그만큼 강력하고 잠재적으로 더 유연합니다.
JavaScript는 객체에 관한 모든 것인데, 이는 전통적인 의미의 객체, 즉 "상태와 동작을 포함하는 단일 개체"를 의미합니다. 예를 들어 JavaScript의 배열은 여러 값을 포함하고 push, reverse 및 pop 메소드를 포함하는 객체입니다.
var myArray = [1, 2]; myArray.push(3); myArray.reverse(); myArray.pop(); var length = myArray.length;
이제 문제는 푸시 방식이 어디서 유래했느냐는 것입니다. 앞서 언급한 정적 언어는 "클래스 구문"을 사용하여 객체의 구조를 정의하지만, JavaScript는 "클래스 구문"이 없는 언어이므로 Array "클래스" 구문을 사용하여 각 배열 객체를 정의할 수 없습니다. 그리고 JavaScript는 동적 언어이기 때문에 필요할 때 객체에 메소드를 임의로 배치할 수 있습니다. 예를 들어, 다음 코드는 2차원 공간에서 점을 나타내는 데 사용되는 점 개체를 정의하고 add 메서드도 정의합니다.
var point = { x : 10, y : 5, add: function(otherPoint) { this.x += otherPoint.x; this.y += otherPoint.y; } };
하지만 위 접근 방식의 확장성은 좋지 않습니다. 각 포인트 개체에 add 메서드가 포함되어 있는지 확인해야 하며, 이 메서드를 각 포인트 개체에 수동으로 추가하는 대신 모든 포인트 개체가 동일한 add 메서드 구현을 공유하기를 바랍니다. 프로토타입 제작이 시작되는 곳입니다.
JavaScript에서 각 객체는 숨겨진 상태 부분, 즉 프로토타입이라고도 불리는 다른 객체에 대한 참조를 유지합니다. 이전에 만든 배열은 우리가 직접 만든 포인트 개체와 마찬가지로 프로토타입 개체를 참조합니다. 위에서는 프로토타입 참조가 숨겨져 있다고 말했지만, 객체(예: Google Chrome)의 __proto__ 속성을 통해 이 프로토타입 참조에 액세스할 수 있는 ECMAScript(JavaScript의 공식 이름) 구현도 있습니다. 개념적으로 우리는 객체를 그림 1에 표시된 객체-프로토타입 관계와 유사하다고 생각할 수 있습니다.
사진 1
미래에는 개발자가 __proto__ 속성 대신 Object.getPrototypeOf 함수를 사용하여 객체 프로토타입에 대한 참조를 얻을 수 있을 것입니다. 이 기사를 작성하는 시점에는 Google Chrome, FIrefox 및 IE9 브라우저에서 Object.getPrototypeOf 함수를 이미 사용할 수 있습니다. 이 기능은 이미 ECMAScript 표준의 일부이므로 앞으로 더 많은 브라우저에서 이 기능을 구현할 예정입니다. 다음 코드를 사용하여 우리가 만든 myArray 및 point 개체가 두 개의 서로 다른 프로토타입 개체를 참조한다는 것을 증명할 수 있습니다.
이 기사의 나머지 부분에서는 __proto__ 및 Object.getPrototypeOf 함수를 교차 사용하겠습니다. 그 이유는 __proto__가 다이어그램과 문장에서 인식하기 더 쉽기 때문입니다. 기억해야 할 것은 (__proto__)가 표준이 아니며 Object.getPrototypeOf 함수는 객체의 프로토타입을 보는 데 권장되는 방법이라는 것입니다.
프로토타입이 특별한 이유는 무엇인가요?
우리는 아직 이 질문에 대답하지 않았습니다: 배열의 푸시 메소드는 어디에서 왔습니까? 대답은 myArray 프로토타입 객체에서 비롯된다는 것입니다. 그림 2는 Chrome의 스크립트 디버거 스크린샷입니다. myArray의 프로토타입 객체를 보기 위해 Object.getPrototypeOf 메소드를 호출했습니다.
사진 2
注意 myArray 的原型对象中有许多方法,包括那些在代码示例中调用的 push、pop 和 reverse 方法。因此,原型对象中的确包括 push 方法,但是 myArray 方法如何引用到呢?
myArray.push(3);
了解其工作原理的第一步,是要认识到原型并不是特别的。原型只是普通的对象。可以给原型添加方法,属性,并把他们当作其他 JavaScript 对象一样看待。然而,套用乔治·奥威尔的小说《动物农场》中“猪”的说法 —— 所有的对象应当是平等的,但有些对象(遵守规则的)比其他人更加平等。
JavaScript 中的原型对象的确是特殊的,因为他们遵从以下规则。当我们告诉 JavaScript 我们要调用一个对象的 push 方法,或读取对象的 x 属性时,运行时会首先查找对象本身。如果运行时找不到想要的东西,它就会循着 __proto__ 引用和对象原型寻找该成员。当我们 调用 myArray 的 push 方法时,JavaScript 并没有在 myArray 对象上发现 push 方法,而是在 myArray 的原型对象上找到了,于是 JavaScript 调用此方法(见图 3)。
图 3
上面所描述的行为是指一个对象本身继承了原型上的任何方法或属性。JavaScript 中其实不需要使用类语法也能实现继承。就像从赛车原型上继承了相应的技术的车,一个 JavaScript 对象也可以从原型对象上继承功能特性。
图 3 还展示了每个数组对象同时也可以维护自身的状态和成员。在请求得到 myArray 的 length 属性的情况下,JavaScript 会取得 myArray 中 length 属性的值,而不会去读取原型中的对应值。我们可以通过向对象上添加 push 这样的方法来“重写”push 方法。这样就会有效地隐藏原型中的 push 方法实现。
JavaScript 中原型的真正神奇之处是多个对象如何维持对同一个原型对象的引用。例如,如果我们创建了这样的两个数组:
var myArray = [1, 2]; var yourArray = [4, 5, 6];
那么这两个数组将共享同一个原型对象,而下面的代码计算结果为 true:
Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);
如果我们引用两个数组对象上的 push 方法,JavaScript 会去寻找原型上共享的 push 方法。
图 4
JavaScript 中的原型对象提供继承功能,同时也就实现了该方法实现的共享。原型也是链式的。换句话说,因为原型对象只是一个对象,所以一个原型对象可以维持到另一个原型对象的引用。如果你重新审视图 2 便可以看到,原型的 __proto__ 属性是一个指向另一个原型的非空值。当 JavaScript 查找像 push 方法这样的成员时,它会循着原型引用链检查每一个对象,直到找到该成员,或者抵达原型链的末端。原型链为继承和共享开辟了一条灵活的途径。
你可能会问的下一个问题是:我该如何设置那些自定义对象的原型引用呢?例如前面所使用的点对象,如何才能将 add 方法添加到原型对象中,并从多个点对象中继承方法呢?在回答这个问题之前,我们需要看看函数。
JavaScript 中的函数也是对象。这样的表述带来了几个重要的结果,而我们并不会在本文中涉及所有的事项。这其中,能将一个函数赋值给一个变量,并且将一个函数作为参数传递给另一个函数的能力构成了现代 JavaScript 编程表达的基本范式。
我们需要关注的是,函数本身就是对象,因此函数可以有自身的方法,属性,并且引用一个原型对象。让我们来讨论下面的代码的含义。
// 这将返回 true: typeof (Array) === "function" // 这样的表达式也是: Object.getPrototypeOf(Array) === Object.getPrototypeOf(function () { }) // 这样的表达式同样: Array.prototype != null
代码中的第一行证明, JavaScript 中的数组是函数。稍后我们将看到如何调用 Array 函数创建一个新的数组对象。下一行代码,证明了 Array 对象使用与任何其他函数对象相同的原型,就像我们看到数组对象间共享相同的原型一样。最后一行代码证明了 Array 函数都有一个 prototype 属性,而这个 prototype 属性指向一个有效的对象。这个 prototype 属性十分重要。
JavaScript 中的每一个函数对象都有 prototype 属性。千万不要混淆这个 prototype 属性的 __proto__ 属性。他们用途并不相同,也不是指向同一个对象。
// 返回 true Object.getPrototypeOf(Array) != Array.prototype
Array.__proto__ 提供的是 数组原型 – 请把它当作 Array 函数所继承的对象。
而 Array.protoype,提供的的是 所有数组的原型对象。也就是说,它提供的是像 myArray 这样数组对象的原型对象,也包含了所有数组将会继承的方法。我们可以写一些代码来证明这个事实。
// true Array.prototype == Object.getPrototypeOf(myArray) // 也是 true Array.prototype == Object.getPrototypeOf(yourArray);
我们也可以使用这项新知识重绘之前的示意图。
图 5
基于所知道的知识,请想象创建一个新的对象,并让新对象表现地像数组的过程。一种方法是使用下面的代码。
// 创建一个新的空对象 var o = {}; // 继承自同一个原型,一个数组对象 o.__proto__ = Array.prototype; // 现在我们可以调用数组的任何方法... o.push(3);
虽然这段代码很有趣,也能工作,可问题在于,并不是每一个 JavaScript 环境都支持可写的 __proto__ 对象属性。幸运的是,JavaScript 确实有一个创建对象内建的标准机制,只需要一个操作符,就可以创建新对象,并且设置新对象的 __proto__ 引用 – 那就是“new”操作符。
var o = new Array(); o.push(3);
JavaScript 中的 new 操作符有三个基本任务。首先,它创建新的空对象。接下来,它将设置新对象的 __proto__ 属性,以匹配所调用函数的原型属性。最后,操作符调用函数,将新对象作为“this”引用传递。如果要扩展最后两行代码,就会变成如下情况:
var o = {}; o.__proto__ = Array.prototype; Array.call(o); o.push(3);
函数的 call 方法允许你在调用函数的情况下在函数内部指定“this”所引用的对象。当然,函数的作者在这种情况下需要实现这样的函数。一旦作者创建了这样的函数,就可以将其称之为构造函数。
构造函数
构造函数和普通的函数一样,但是具有以下两个特殊性质。
Array 就是一个构造函数的例子。Array 函数需要和 new 操作符一起使用,而且 Array 的首字母是大写的。JavaScript 将 Array 作为内置函数包括在内,而任何人都可以写出自己的构造函数。事实上,我们最后可以为先前创建的点对象编写出构造函数。
var Point = function (x, y) { this.x = x; this.y = y; this.add = function (otherPoint) { this.x += otherPoint.x; this.y += otherPoint.y; } } var p1 = new Point(3, 4); var p2 = new Point(8, 6); p1.add(p2);
在上面的代码中,我们使用了 new 操作符和 Point 函数来构造点对象,这个对象带有 x 属性和 y 属性和一个 add 方法。你可以将最后的结果想象成图 6 的样子。
图 6
现在的问题是我们的每个点对象中仍然有单独的 add 方法。使用我们学到的原型和继承的知识,我们更希望将点对象的 add 方法从每个点实例中转移到 Point.prototype 中。要达到继承 add 方法的效果,我们所需要做的,就是修改 Point.prototype 对象。
var Point = function (x, y) { this.x = x; this.y = y; } Point.prototype.add = function (otherPoint) { this.x += otherPoint.x; this.y += otherPoint.y; } var p1 = new Point(3, 4); var p2 = new Point(8, 6); p1.add(p2);
大功告成!我们刚刚在 JavaScript 中完成原型式的继承模式!
图 7
总结
我希望这篇文章能够帮助你揭开 JavaScript 原型概念的神秘面纱。开始看到的是原型怎样让一个对象从其他对象中继承功能,然后看到怎样结合 new 操作符和构造函数来构建对象。这里所提到的,只是开启对象原型力量和灵活性的第一步。本文鼓励你自己发现学习有关原型和 JavaScript 语言的新信息。
同时,请小心驾驶。你永远不会知道这些行驶在路上的车辆会从他们的原型继承到什么(有缺陷)的技术。
原文链接: Script Junkie 翻译: 伯乐在线 - 埃姆杰