>웹 프론트엔드 >JS 튜토리얼 >Javascript 상속_javascript 기술 구현에 대한 자세한 설명

Javascript 상속_javascript 기술 구현에 대한 자세한 설명

WBOY
WBOY원래의
2016-05-16 15:08:191395검색

이 기사에서는 다음 네 가지 측면에서 주제를 논의합니다.

•1. 혼합적 접근방식 구현과 문제점

•2. 예상 호출 방법

•3.상속 라이브러리 상세 구현

•4. 요약

관심 있는 친구는 계속해서 자세한 내용을 읽어볼 수 있습니다.

JS에서 상속을 구현하기 위해 가장 먼저 익힌 방법은 xx에서 배운 프로토타입 체인과 객체 가장을 혼합하는 방법이었습니다. 직장에서 상속을 사용할 때마다 항상 이 방법을 사용하여 구현합니다. 구현은 간단하고 아이디어는 분명합니다. 객체를 사용하여 상위 클래스 생성자의 속성을 상속하는 척하고, 프로토타입 체인을 사용하여 상위 클래스 프로토타입 객체의 메서드를 상속합니다. 이는 제가 접한 모든 상속 시나리오를 충족합니다. 이 때문에 다음 번에 상속에 관해 글을 쓸 때 다른 방식으로 작성해야 한다는 생각은 전혀 하지 못했습니다. 오늘 밤까지 저는 Sanshengshi에서 JavaScript 상속에 관한 일련의 기사를 읽었습니다. 지금은 정말 조금 아쉽습니다), js에서 상속 메커니즘이 Java와 같은 백엔드 언어의 구현과 매우 유사하게 작성될 수도 있다는 것을 발견했습니다. 이는 정말 훌륭합니다! 그래서 나는 그의 블로그의 아이디어를 충분히 이해하고 앞으로 사용할 수 있는 상속 라이브러리를 구현하고 싶습니다.

1. 혼합적 접근방식 구현과 문제점

문제를 이해하기 전에 구체적인 구현을 살펴보겠습니다.

- Hide code
//父类构造函数
function Employee(name, salary) {
//实例属性:姓名
this.name = name;
//实例属性:薪资
this.salary = salary;
}
//通过字面量对象设置父类的原型,给父类添加实例方法
Employee.prototype = {
//由于此处添加实例方法时也是通过修改父类原型处理的,
//所以必须修改父类原型的constructor指向,避免父类实例的constructor属性指向Object函数
constructor: Employee,
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
}
//子类构造函数
function Manager(name, salary, percentage) {
//对象冒充,实现属性继承(name, salary)
Employee.apply(this, [name, salary]);
//实例属性:提成
this.percentage = percentage;
}
//将父类的一个实例设置为子类的原型,实现方法继承
Manager.prototype = new Employee();
//修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数
Manager.prototype.constructor = Manager;
//给子类添加新的实例方法
Manager.prototype.getSalary = function () {
return this.salary + this.salary * this.percentage;
}
var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);
console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //true
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false

결과적으로 보면 이번 상속 구현에는 문제가 없습니다. Manager 인스턴스는 Employee 클래스의 인스턴스 속성과 인스턴스 메소드를 동시에 상속받으며, instanceOf 연산의 결과도 정확합니다. 그러나 코드 구성 및 구현 세부 사항 측면에서 이 방법에는 여전히 다음과 같은 문제가 있습니다.

1) 코드 구성은 충분히 우아하지 않습니다. 상속 구현의 핵심 부분 논리는 보편적이며 다음과 같은 구조를 갖습니다.

- Hide code
//将父类的一个实例设置为子类的原型,实现方法继承
SubClass.prototype = new SuperClass();
//修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数
SubClass.prototype.constructor = SubClass;
//给子类添加新的实例方法
SubClass.prototype.method1 = function() {
}
SubClass.prototype.method2 = function() {
}
SubClass.prototype.method3 = function() {
}

이 코드에는 캡슐화가 부족합니다. 또한, 서브클래스의 인스턴스 메소드를 추가할 때 SubClass.prototype = { method1: function() {} } 을 통해 설정할 수 없습니다. 그렇지 않으면 서브클래스의 프로토타입 전체가 수정되어 상속이 불가능해집니다. 이렇게 하면 SubClass.prototype.method1 = function() {}의 구조에 따라 작성해야 할 때마다 코드가 매우 불연속적으로 보입니다.

해결책: 모듈화를 사용하여 공통 논리를 캡슐화하고 합의된 인터페이스에 따라 호출하는 한 클래스의 구성과 상속을 단순화할 수 있습니다. 구체적인 구현에 대해서는 다음 내용 소개를 참조하세요. 당분간은 이론적인 설명만 제공될 수 있습니다.

2) 하위 클래스의 프로토타입을 상위 클래스의 인스턴스로 설정할 때 new SuperClass()가 호출됩니다. 이는 상위 클래스 생성자에 대한 매개변수 없는 호출이므로 상위 클래스에는 매개변수 없는 생성자 함수가 있어야 합니다. 그러나 JavaScript에서는 함수를 오버로딩할 수 없으므로 부모 클래스가 여러 생성자를 제공하는 것이 불가능합니다. 실제 비즈니스에서는 대부분의 시나리오에서 부모 클래스 생성자는 유일한 생성자에서 함수를 시뮬레이션하기 위해 매개 변수를 가질 수 없습니다. 오버로드는 인수.길이를 판단해야만 처리할 수 있습니다. 문제는 부모 클래스 생성자가 작성될 때마다 Argument.length 판단 논리가 추가되는지 확인하기 어려운 경우가 있다는 것입니다. 이 경우 이 접근 방식은 위험합니다. 생성자의 로직을 추출할 수 있고 클래스의 모든 생성자가 매개변수 없는 함수라면 이 문제는 쉽게 해결됩니다.

해결책: 상위 클래스와 하위 클래스의 모든 생성자를 매개변수 없이 만들고 생성자에 로직을 작성하지 마세요. 생성자의 로직을 Employee 및 Manager와 같은 init 인스턴스 메소드로 마이그레이션하세요. 다음과 같이:

- Hide code
//无参无逻辑的父类构造函数
function Employee() {}
Employee.prototype = {
constructor: Employee,
//把构造逻辑搬到init方法中来
init: function (name, salary) {
this.name = name;
this.salary = salary;
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
};
//无参无逻辑的子类构造函数
function Manager() {}
Manager.prototype = new Employee();
Manager.prototype.constructor = Manager;
//把构造逻辑搬到init方法中来
Manager.prototype.init = function (name, salary, percentage) {
//借用父类的init方法,实现属性继承(name, salary)
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
};
Manager.prototype.getSalary = function () {
return this.salary + this.salary * this.percentage;
};

init 메소드를 사용하여 구성 함수를 완성하면 하위 클래스의 프로토타입(Manager.prototype = new Employee())을 설정할 때 상위 클래스의 인스턴스화 작업이 잘못되지 않도록 할 수 있습니다. 클래스를 호출할 때 생성자를 사용하여 인스턴스를 초기화하는 경우 생성자를 호출한 후 수동으로 init 메서드를 호출하여 실제 생성 논리를 완료해야 합니다.

- Hide code
var e = new Employee();
e.init('jason', 5000);
var m = new Manager();
m.init('tom', 8000, 0.15);
console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //true
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false

이 init의 논리를 생성자 내부에 배치할 수 있으면 좋겠지만 이는 앞서 언급한 생성자에 매개변수와 논리가 없다는 원칙을 위반하는 것입니다. 다른 방식으로 생각해보세요. 이 원칙의 목적은 상위 클래스를 하위 클래스의 프로토타입으로 인스턴스화할 때 상위 클래스의 생성자를 호출할 때 오류가 발생하지 않도록 하는 것입니다. 그러면 이 원칙을 약간 깨뜨릴 수 있습니다. 클래스 생성자에서 이를 사용하여 문제를 확실히 해결할 수 있는 약간의 로직을 추가하세요.

- Hide code
//添加一个全局标识initializing,表示是否正在进行子类的构建和类的继承
var initializing = false;
//可自动调用init方法的父类构造函数
function Employee() {
if (!initializing) {
this.init.apply(this, arguments);
}
}
Employee.prototype = {
constructor: Employee,
//把构造逻辑搬到init方法中来
init: function (name, salary) {
this.name = name;
this.salary = salary;
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
};
//可自动调用init方法的子类构造函数
function Manager() {
if (!initializing) {
this.init.apply(this, arguments);
}
}
//表示开始子类的构建和类的继承
initializing = true;
//此时调用new Emplyee(),并不会调用Employee.prototype.init方法
Manager.prototype = new Employee();
Manager.prototype.constructor = Manager;
//表示结束子类的构建和类的继承,之后调用new Employee或new Manager都会自动调用init实例方法
initializing = false;
//把构造逻辑搬到init方法中来
Manager.prototype.init = function (name, salary, percentage) {
//借用父类的init方法,实现属性继承(name, salary)
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
};
Manager.prototype.getSalary = function () {
return this.salary + this.salary * this.percentage;
};

통화 결과는 여전히 이전 예시와 동일합니다. 하지만 이 구현에는 여전히 작은 문제가 있습니다. 이 전역 변수를 도입할 수 있다면 좋을 것입니다. 클래스의 구성과 상속을 캡슐화하기만 하면 됩니다. 모듈을 선택한 다음 이 변수를 모듈 안에 넣으면 문제가 없습니다.

3) 하위 클래스를 생성할 때 하위 클래스의 프로토타입은 상위 클래스의 인스턴스로 설정됩니다. 이는 클래스와 인스턴스 사이가 아닌 클래스 간에 상속이 발생해야 한다는 의미입니다. 상위 클래스의 인스턴스를 하위 클래스의 프로토타입으로 사용하는 이유:

- Hide code
SubClass.prototype = new SuperClass();

完全是因为父类的这个实例,指向父类的原型,而子类的实例又会指向子类的原型,所以最终子类的实例就能通过原型链访问到父类原型上的方法。这个做法虽然能实现实例方法的继承,但是它不符合语义,而且它还有一个很大的问题就是会增加原型链的长度,导致子类在调用父类方法时,必须通过原型链的查找到父类的方法才行。要是继承层次较深,会对js的执行性能有些影响。

解决方式:在解决这个问题之前,先想想继承能帮我们解决什么问题:从父类复用已有的实例属性和实例方法。在javascript面向对象编程中,一直有一个原则就是,实例属性都写在构造函数或者实例方法里面,实例方法写在原型上面,也就是说类的原型,按照这个原则来说,就是用来写实例方法的,而且是只用来写实例方法,那么我们完全可以在构建子类时,通过复制的方式将父类原型的所有方法全部添加到子类的原型上,不一定要把父类的一个实例设置成子类的原型,这样就能将原型链的长度大大地缩短,借助一个简短的copy函数,我们就能轻松对前面的代码进行改造:

- Hide code
//用来复制父类原型,由于父类原型上约定只写实例方法,所以复制的时候不必担心引用的问题
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
function Employee() {
this.init.apply(this, arguments);
}
Employee.prototype = {
constructor: Employee,
init: function (name, salary) {
this.name = name;
this.salary = salary;
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
};
function Manager() {
this.init.apply(this, arguments);
}
//将父类的原型方法复制到子类的原型上
Manager.prototype = copy(Employee.prototype);
//子类还是需要修改constructor指向,因为从父类原型复制出来的对象的constructor还是指向父类的构造函数
Manager.prototype.constructor = Manager;
Manager.prototype.init = function (name, salary, percentage) {
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
};
Manager.prototype.getSalary = function () {
return this.salary + this.salary * this.percentage;
};
var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);
console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false

这么做了以后,当调用m.toString的时候其实调用的是Manager类自身原型上的方法,而不是Employee类的实例方法,缩短了在原型链上查找方法的距离。这个做法在性能上有很大的优点,但不好的是通过原型链维持的继承关系其实已经断了,子类的原型和子类的实例都无法再通过js原生的属性访问到父类的原型,所以这个调用console.log(m instanceof Employee)输出的是false。不过跟性能比起来,这个都可以不算问题:一是instanceOf的运算,几乎在javascript的开发里面用不到,至少我是没碰到过;二是通过复制方式完全能够把父类的实例方法继承下来,这就已经达到了继承的最大目的。

这个方法还有一个额外的好处是,解决了第2个问题最后提到的引入initializing全局变量的问题,如果是复制的话,就不需要在构建继承关系时,去调用父类的构造函数,那么也就没有必要在构造函数内先判断initializing才能去调用init方法,上面的代码中就已经去掉了initializing这个变量的处理。

4)在子类的构造函数和实例方法内如果想要调用父类的构造函数或者方法,显得比较繁琐:

- Hide code
function SuperClass() {}
SuperClass.prototype = {
constructor: SuperClass,
method1: function () {}
}
function SubClass() {
//调用父类构造函数
SuperClass.apply(this);
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method1 = function () {
//调用父类的实例方法
SuperClass.prototype.method1.apply(this, arguments);
}
SubClass.prototype.method2 = function () {}
SubClass.prototype.method3 = function () {}

每次都得靠apply借用方法来处理。要是能改成如下的调用就好用多了:

- Hide code
function SubClass() {
//调用父类构造函数
this.base();
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method1 = function() {
//调用父类的实例方法
this.base();
}

解决方式:如果要在每个实例方法里,都能通过this.base()调用父类原型上相应的方法,那么this.base就一定不是一个固定的方法,需要在每个实例方法执行期间动态地将this.base指定为父类原型的同名方法,能够做到这个实现的方式,就只有通过方法代理了,前面的Employee和Manager的例子可以改造如下:

- Hide code
//用来复制父类原型,由于父类原型上约定只写实例方法,所以复制的时候不必担心引用的问题
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
};
function Employee() {
this.init.apply(this, arguments);
}
Employee.prototype = {
constructor: Employee,
init: function (name, salary) {
this.name = name;
this.salary = salary;
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
};
function Manager() {
//必须在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型
//因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型
this.baseProto = Employee.prototype;
this.init.apply(this, arguments);
}
Manager.prototype = copy(Employee.prototype);
//子类还是需要修改constructor指向,因为从父类原型复制出来的对象的constructor还是指向父类的构造函数
Manager.prototype.constructor = Manager;
Manager.prototype.init = (function (name, func) {
return function () {
//记录实例原有的this.base的值
var old = this.base;
//将实例的this.base指向父类的原型的同名方法
this.base = this.baseProto[name];
//调用子类自身定义的init方法,也就是func参数传递进来的函数
var ret = func.apply(this, arguments);
//还原实例原有的this.base的值
this.base = old;
return ret;
}
})('init', function (name, salary, percentage) {
//通过this.base调用父类的init方法
//这个函数真实的调用位置是var ret = func.apply(this, arguments);
//当调用Manager实例的init方法时,其实不是调用的这个函数
//而是调用上面那个匿名函数里面return的匿名函数
//在return的匿名函数里,先把this.base指向为了父类原型的同名函数,然后在调用func
//func内部再通过调用this.base时,就能调用父类的原型方法。
this.base(name, salary);
this.percentage = percentage;
});
Manager.prototype.getSalary = function () {
return this.salary + this.salary * this.percentage;
};
var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);
console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false

通过代理的方式,就解决了在在实例方法内部通过this.base调用父类原型同名方法的问题。可是在实际情况中,每个实例方法都有可能需要调用父类的实例,那么每个实例方法都要添加同样的代码,显然这会增加很多麻烦,好在这部分的逻辑也是同样的,我们可以把它抽象一下,最后都放到模块化的内部去,这样就能简化代理的工作。

5)未考虑静态属性和静态方法。尽管静态成员是不需要继承的,但在有些场景下,我们还是需要静态成员,所以得考虑静态成员应该添加在哪里。

解决方式:由于js原生并不支持静态成员,所以只能借助一些公共的位置来处理。最佳的位置是添加到构造函数上:

- Hide code
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
};
function Employee() {
this.init.apply(this, arguments);
}
//添加一个静态属性
Employee.idCounter = 1;
//添加一个静态方法
Employee.getId = function () {
return Employee.idCounter++;
};
Employee.prototype = {
constructor: Employee,
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
};
function Manager() {
this.baseProto = Employee.prototype;
this.init.apply(this, arguments);
}
Manager.prototype = copy(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.init = (function (name, func) {
return function () {
var old = this.base;
this.base = this.baseProto[name];
var ret = func.apply(this, arguments);
this.base = old;
return ret;
}
})('init', function (name, salary, percentage) {
this.base(name, salary);
this.percentage = percentage;
});
Manager.prototype.getSalary = function () {
return this.salary + this.salary * this.percentage;
};
var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);
console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
console.log(m.id); //2
console.log(e.id); //1

最后的两行输出了正确的实例id,而这个id是通过Employee类的静态方法生成的。在java的面向对象编程中,子类跟父类都可以定义静态成员,在调用的时候还存在覆盖的问题,在js里面,因为受语言的限制,自定义的静态成员不可能实现全面的面向对象功能,就像上面这种,能够给类提供一些公共的属性和公共方法,就已经足够了。

2. 期望的调用方式

从第1部分的分析可以看出,在js里面,类的构建与继承,有很多通用的逻辑,完全可以把这些逻辑封装成一个单独的模块,形成一个通用的类库,以便在工作中有需要的时候,都可以直接拿来使用。这个类库要求能完成我们需要的功能(类的构建与继承和静态成员的添加),同时在使用时要足够简洁方便。在利用bootstrap的modal组件自定义alert,confirm和modal对话框这篇文章里,我曾说过一些从组件期望的调用方式,去反推组件实现的一些观点,当你明确你需要什么东西时,你才知道这个东西你该怎么去创造。本文要编写的这个继承组件也会采取这个方法来实现,我先用前面Employee和Manager的例子来模拟调用这个继承库的场景,通过预设的一些组件名称或者接口名称以及调用方式,来尝试走通真实使用这个继承库的流程,有了这个东西,下一步我只需要根据这个要求去实现即可,模拟如下:

- Hide code
//通过调用Class函数构造一个类
var Employee = Class({
//通过instanceMembers指定这个类的实例成员
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
},
//通过staticMembers指定这个类的静态成员
//静态方法内部可通过this访问其它静态成员
//在外部可通过Employee.getId这种方式访问到静态成员
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
this.base(name, salary);
this.percentage = percentage;
Manager.count++;
},
getSalary: function () {
return this.salary + this.salary * this.percentage;
}
},
//通过extend指定要继承的类
extend: Employee
});

从模拟的结果来看,我想要的继承库对外提供的名称只有Class, instanceMembers, staticMembers和extend而已,调用方式也很简单,只要传递参数给Class函数即可。接下来就按照这个目标,看看如何一步步根据第一部分罗列的那些问题和解决方式,把这个库给写出来。

3. 继承库的详细实现

根据API名称和接口以及前面第1部分提出的问题,这个继承库要完成的功能有:
1)类的构建(关键:init方法)和静态成员处理;
2)继承关系的构建(关键:父类原型的复制);
3)父类方法的简化调用(关键:父类原型上同名方法的代理)。
所以这个库的实现,可以按照这三点分成三版来开发。

1)第一版

在第一版里面,仅需要实现类的构架和静态成员添加的功能即可,细节如下:

- Hide code
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用来判断是否为Object的实例
function isObject(o) {
return typeof (o) === 'object';
}
//用来判断是否为Function的实例
function isFunction(f) {
return typeof (f) === 'function';
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) && options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要构建的类的构造函数
function TargetClass() {
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
TargetClass.prototype = instanceMembers;
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();

这一版核心代码在于类的构建和静态成员添加的部分,其它代码仅仅提供一些提前可以想到的赋值函数和变量(isObject, isFunction),并做一些参数合法性校验的处理。添加静态成员的代码一定要在设置原型的代码之前,否则就有原型被覆盖的风险。有了这个版本,就可以直接构建带静态成员的Employee类了:

- Hide code
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var e = new Employee('jason', 5000);
console.log(e.toString()); //jason's salary is 5000.
console.log(e.id); //1
console.log(e.constructor === Employee); //true

在getId方法中之所以直接使用this就能访问到构造函数Employee,是因为getId这个方法是添加到构造函数上的,所以当调用Employee.getId()时,getId方法里面的this指向的就是Employee这个函数对象。

第二版在第一版的基础上,实现继承关系的构建部分:

- Hide code
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用来判断是否为Object的实例
function isObject(o) {
return typeof (o) === 'object';
}
//用来判断是否为Function的实例
function isFunction(f) {
return typeof (f) === 'function';
}
//简单复制
function copy(source) {
var target = {};
for (var i in source) {
if (hasOwn.call(source, i)) {
target[i] = source[i];
}
}
return target;
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) && options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要构建的类的构造函数
function TargetClass() {
if (extend) {
//如果有要继承的父类
//就在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型
//因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型
this.baseProto = extend.prototype;
}
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
//如果有要继承的父类,先把父类的实例方法都复制过来
extend && (TargetClass.prototype = copy(extend.prototype));
//添加实例方法
for (prop in instanceMembers) {
if (hasOwn.call(instanceMembers, prop)) {
TargetClass.prototype[prop] = instanceMembers[prop];
}
}
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();

这一版关键的部分在于:

this.baseProto主要目的就是为了让子类的实例能够有一个属性可以访问到父类的原型,因为后面的继承方式是复制方式,会导致原型链断裂。有了这一版之后,就可以加入Manager类来演示效果了:

- Hide code
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
//借用父类的init方法,实现属性继承(name, salary)
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
},
getSalary: function () {
return this.salary + this.salary * this.percentage;
}
},
extend: Employee
});
var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);
console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(e.constructor === Employee); //true
console.log(m.constructor === Manager); //true
console.log(e.id); //1
console.log(m.id); //2

不过在Manager内部,调用父类的方法时还是apply借用的方式,所以在最后一版里面,需要把它变成我们期望的this.base的方式,反正原理前面也已经了解了,无非是在方法同名的时候,对实例方法加一个代理而已,实现如下:

- Hide code
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用来判断是否为Object的实例
function isObject(o) {
return typeof (o) === 'object';
}
//用来判断是否为Function的实例
function isFunction(f) {
return typeof (f) === 'function';
}
//简单复制
function copy(source) {
var target = {};
for (var i in source) {
if (hasOwn.call(source, i)) {
target[i] = source[i];
}
}
return target;
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) && options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要构建的类的构造函数
function TargetClass() {
if (extend) {
//如果有要继承的父类
//就在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型
//因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型
this.baseProto = extend.prototype;
}
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
//如果有要继承的父类,先把父类的实例方法都复制过来
extend && (TargetClass.prototype = copy(extend.prototype));
//添加实例方法
for (prop in instanceMembers) {
if (hasOwn.call(instanceMembers, prop)) {
//如果有要继承的父类,且在父类的原型上存在当前实例方法同名的方法
if (extend && isFunction(instanceMembers[prop]) && isFunction(extend.prototype[prop])) {
TargetClass.prototype[prop] = (function (name, func) {
return function () {
//记录实例原有的this.base的值
var old = this.base;
//将实例的this.base指向父类的原型的同名方法
this.base = this.baseProto[name];
//调用子类自身定义的实例方法,也就是func参数传递进来的函数
var ret = func.apply(this, arguments);
//还原实例原有的this.base的值
this.base = old;
return ret;
}
})(prop, instanceMembers[prop]);
} else {
TargetClass.prototype[prop] = instanceMembers[prop];
}
}
}
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();

核心部分是:

只有当需要继承父类,且父类原型中有方法与当前的实例方法同名时,才会去对当前的实例方法添加代理。更详细的原理可以回到文章第1部分回顾相关内容。至此,我们在Manager类内部调用父类的方法时,就很简单了,只要通过this.base即可:

- Hide code
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
//通过this.base调用父类的构造方法
this.base(name, salary);
this.percentage = percentage;
},
getSalary: function () {
return this.base() + this.salary * this.percentage;
}
},
extend: Employee
});
var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);
console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(e.constructor === Employee); //true
console.log(m.constructor === Manager); //true
console.log(e.id); //1
console.log(m.id); //2

다음 두 가지 호출에 주의하세요.

위 내용은 이 기사에서 구현할 상속 라이브러리의 모든 세부 사항입니다. 실제로 이는 이 기사의 첫 번째 부분에서 언급한 문제에 대한 솔루션과 두 번째 부분에서 시뮬레이션한 호출 시나리오를 결합하는 것입니다. 각 세부 사항의 원리는 Part 1에 요약된 솔루션을 이해하는 한 쉽게 이해할 수 있습니다. 데모의 마지막 버전에서는 이 기사에서 구현된 상속 라이브러리가 시뮬레이션 시나리오의 요구 사항을 완전히 충족했음을 확인할 수 있습니다. 향후 상속이 필요한 시나리오가 있는 경우 구현의 마지막 버전을 사용할 수 있습니다. 그것을 개발하기 위해.

4. 요약

Sanshengshi의 JavaScript 상속에 관한 일련의 블로그 지침에 따라 이 기사는 Java 언어와 유사한 객체 지향 클래스 및 클래스 간 상속 관계를 구축하는 데 사용할 수 있는 사용하기 쉬운 상속 라이브러리를 구현합니다. 내 향후 작업에서 이 라이브러리는 내 코드 품질과 기능 구현에 매우 중요한 역할을 할 것으로 예상할 수 있습니다. 왜냐하면 개발 과정에서 상속된 코딩 아이디어가 특히 많은 프로젝트를 수행할 때 여전히 많이 사용되기 때문입니다. 한편, 나는 재사용 가능한 컴포넌트에 일부 공개적인 것을 작성하고 싶습니다. 반면에 각 프로젝트의 개별 요구 사항을 충족해야 하므로 컴포넌트를 작성할 때 너무 엄격하게 작성할 수 없고 더 많은 인터페이스를 작성하고 완료될 때까지 기다릴 수 없습니다. 특정 프로젝트가 완료되면 상속 및 기타 방법을 통해 프로젝트의 고유한 기능을 확장하여 작성된 구성 요소를 더욱 유연하고 안정적으로 만들 수 있습니다. 요컨대, 이 상속 라이브러리를 사용하면 앞으로 제가 작성하는 코드가 훨씬 더 행복해질 것 같은 느낌이 듭니다~ 그래서 이 글의 내용도 여러분에게 같은 도움이 되기를 바랍니다. 정말 도움이 되셨다면 추천 부탁드려요 :)

자바스크립트 상속 구현에 대해 에디터가 이것저것 소개해 드릴게요. 도움이 되셨으면 좋겠습니다!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.