Javascript는 프로토타입 기반 프로그래밍 언어로, 일반적인 클래스 기반 프로그래밍과 매우 다릅니다. 다음과 같이 중요한 사항을 나열하겠습니다.
1. 함수는 일급 객체입니다. 즉, 함수와 객체는 동일한 언어 상태를 가집니다
2. 클래스는 없고 객체만 있습니다
3. 함수도 일종의 객체, 이른바 함수객체
4. 객체는 참조로 전달됩니다
그렇다면 이 프로토타입 기반 프로그래밍 언어는 프로토타입의 근원인 상속(OO의 기본 요소)을 어떻게 구현하는가.
아래 코드 스니펫을 살펴보세요.
function foo(a, b, c) { return a*b*c; } alert(foo.length); alert(typeof foo.constructor); alert(typeof foo.call); alert(typeof foo.apply); alert(typeof foo.prototype);
위 코드의 경우 브라우저에서 실행하면 다음을 찾을 수 있습니다.
1.length: 함수의 매개변수 개수를 제공합니다
2.프로토타입: 객체입니다
3. 나머지 3개는 함수입니다
함수 선언의 경우 위에서 언급한 5가지 속성(메서드 또는 속성)을 갖습니다.
시제품을 살펴보겠습니다.
// prototype function Person(name, gender) { this.name = name; this.gender = gender; this.whoAreYou = function(){//这个也是所谓的closure, 内部函数可以访问外部函数的变量 var res = "I'm " + this.name + " and I'm a " + this.gender +"."; return res; }; } // 那么在由Person创建的对象便具有了下面的几个属性 Person.prototype.age = 24; Person.prototype.getAge = function(){ return this.age; }; flag = true; if (flag) { var fun = new Person("Tower", "male"); alert(fun.name); alert(fun.gender); alert(fun.whoAreYou()); alert(fun.getAge()); } Person.prototype.salary = 10000; Person.prototype.getSalary = function(){ return this.name + " can earn about " + this.salary + "RMB each month." ; }; // 下面就是最神奇的地方, 我们改变了Person的prototype,而这个改变是在创建fun之后 // 而这个改变使得fun也具有了相同的属性和方法 // 继承的意味即此 if (flag) { alert(fun.getSalary()); alert(fun.constructor.prototype.age);//而这个相当于你直接调用 Person.prototype.age alert(Person.prototype.age); }
위의 예에서 프로토타입의 메소드나 속성을 동적으로 추가할 수 있으며, 이에 의해 생성된 객체는 관련 메소드와 속성을 자동으로 상속한다는 것을 알 수 있습니다.
또한 각 객체에는 해당 객체를 생성한 함수 객체를 가리키는 데 사용되는 생성자 속성이 있습니다. 예를 들어 위 예제의 fun.constructor는 Person을 가리킵니다.
그러면 자연스럽게 의문이 생깁니다. 함수 객체가 선언한 메소드와 속성과 프로토타입이 선언한 객체의 차이점은 무엇일까요?
몇 가지 차이점이 있습니다.
1. 직접 선언한 메소드와 속성은 정적입니다. 즉, 새로운 메소드를 추가하거나 기존 메소드를 선언한 후 수정하려고 해도 해당 메소드가 생성한 객체에 영향을 미치지 않으며 생성된 객체에도 영향을 미치지 않습니다. 즉, 상속이 실패했습니다
2. 프로토타입은 동적으로 새 메소드를 추가하거나 기존 메소드를 수정할 수 있으므로 동적입니다. 상위 함수 객체가 관련 프로토타입 속성을 선언하면 생성된 객체는 자동으로 이러한 프로토타입 속성을 상속합니다.
위의 예를 계속해서 설명합니다.
flag = true; // 函数内部声明的方法是静态的,无法传递的 Person.school = "ISCAS"; Person.whoAreYou = function(){ return "zhutao"; };//动态更改声明期的方法,并不会影响由其创建的对象的方法, 即所谓的 静态 if (flag) { alert(Person.school); alert(fun.school);//输出的是 "undefined" alert(Person.whoAreYou()); //输出 zhutao alert(fun.whoAreYou()); // I'm Tower and I'm a male. } Person.prototype.getSalary = function(){ return "I can earn 1000000 USD"; }; if (flag) { alert(fun.getSalary());//已经继承了改变, 即所谓的 动态 }
함수 객체 자체의 속성과 프로토타입의 속성이 있는데, 그에 의해 생성된 객체는 해당 속성을 어떻게 검색하나요?
기본적으로 아래의 과정과 순서를 따라주세요.
1. 먼저 함수 객체 자체의 속성을 검색하고, 발견되면 즉시 실행합니다
2. 1이 발견되지 않으면 프로토타입 속성을 검색합니다. 결과가 2개입니다. 발견되면 직접 실행됩니다. 그렇지 않으면 상위 객체의 상위 객체의 프로토타입을 검색합니다. 프로토타입 체인의 끝을 찾거나 도달합니다(끝은 Object 객체가 됩니다)
위의 내용은 함수 개체 자체의 속성이 프로토타입 속성(중복 이름)과 동일한 경우 함수 자체의 개체가 우선 적용되는 경우 솔루션에 대한 답변이기도 합니다.
전형적인 프로토타입 예시
jQuery나 Prototype 라이브러리를 사용해 본 친구들은 이러한 라이브러리에 일반적으로 Trim 메서드가 있다는 것을 알 수 있습니다.
예:
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); };
트림 사용법:
' foo bar '.trim(); // 'foo bar'
그러나 이 작업에는 또 다른 단점이 있습니다. 최신 버전의 브라우저에 있는 JavaScript 엔진 자체가 String 개체에 트림 메서드를 제공하므로 우리가 정의한 트림이 자체 트림을 덮어쓰게 되기 때문입니다. 실제로 트림 메소드를 정의하기 전에 이 메소드를 직접 추가해야 하는지 간단한 테스트를 통해 확인할 수 있습니다.
if(!String.prototype.trim) { String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }; }
프로토타입 체인
JavaScript에서 객체가 정의되거나 인스턴스화되면 __proto__라는 숨겨진 속성이 추가되며 프로토타입 체인은 이 속성을 사용하여 형성됩니다. 그러나 일부 브라우저는 직접 액세스를 지원하지 않으므로 __proto__ 속성에 직접 액세스하지 마십시오. 또한 __proto__와 객체의 프로토타입 속성은 서로 다른 목적을 가지고 있습니다.
어떻게 이해하나요? 실제로 myObject 함수를 생성할 때 실제로는 Function 유형의 객체를 생성합니다.
console.log(myObject 유형) // 함수
여기서 Function은 JavaScript의 사전 정의된 객체이므로 자체 사전 정의된 속성(예: 길이 및 인수)과 메서드(예: 호출 및 적용)도 있습니다. 프로토타입 체인을 구현합니다. 즉, JavaScript 엔진 내에 다음과 유사한 코드 조각이 있을 수 있습니다.
Function.prototype = { arguments: null, length: 0, call: function() { // secret code }, apply: function(){ // secret code }, ... };
사실 JavaScript 엔진 코드는 그렇게 간단할 수 없습니다. 다음은 프로토타입 체인의 작동 방식에 대한 설명입니다.
우리는 매개변수 이름도 가지고 있지만 길이와 같은 다른 속성이나 호출과 같은 다른 메서드를 제공하지 않는 myObject 함수를 정의합니다. 그러면 다음 코드가 정상적으로 실행되는 이유는 무엇일까요?
console.log(myObject.length); // 结果:1,是参数的个数
这是因为我们定义 myObject 时,同时也给它定义了一个 __proto__ 属性,并赋值为 Function.prototype(参考前面的代码片段),所以我们能够像访问其它属性一样访问 myObject.length,即使我们并没有定义这个属性,因为它会顺着 __proto__ 原型链往上去找 length,最终在 Function 里面找到了。
那为什么找到的 length 属性的值是 1,而不是 0 呢,是什么时候给它赋值的呢?由于 myObject 是 Function 的一个实例:
console.log(myObject instanceof Function); // true console.log(myObject === Function); // false
当实例化一个对象的时候,对象的 __proto__ 属性会被赋值为其构造者的原型对象,在本示例中就是 Function,此时构造器回去计算参数的个数,改变 length 的值。
console.log(myObject.__proto__ === Function.prototype); // true
而当我们用 new 关键字创建一个新的实例时,新对象的 __proto__ 将会被赋值为 myObject.prototype,因为现在的构造函数为 myObject,而非 Function。
var myInstance = new myObject('foo'); console.log(myInstance.__proto__ === myObject.prototype); // true
新对象除了能访问 Function.prototype 中继承下来的 call 和 apply 外,还能访问从 myObject 中继承下来的 getName 方法:
console.log(myInstance.getName()); // foo var mySecondInstance = new myObject('bar'); console.log(mySecondInstance.getName()); // bar console.log(myInstance.getName()); // foo
其实这相当于把原型对象当做一个蓝本,然后可以根据这个蓝本创建 N 个新的对象。
再看一个多重prototype链的例子:
// 多重prototype链的例子 function Employee(name) { this.name = ""; this.dept = "general"; this.gender = "unknown"; } function WorkerBee() { this.projects = []; this.hasCar = false; } WorkerBee.prototype = new Employee; // 第一层prototype链 function Engineer() { this.dept = "engineer"; //覆盖了 "父对象" this.language = "javascript"; } Engineer.prototype = new WorkerBee; // 第二层prototype链 var jay = new Engineer("Jay"); if (flag) { alert(jay.dept); //engineer, 找到的是自己的属性 alert(jay.hasCar); // false, 搜索到的是自己上一层的属性 alert(jay.gender); // unknown, 搜索到的是自己上二层的属性 }
上面这个示例的对象关系如下: