Home > Web Front-end > JS Tutorial > body text

Detailed explanation of the implementation of Javascript inheritance_javascript skills

WBOY
Release: 2016-05-16 15:08:19
Original
1366 people have browsed it

This article discusses the topic from the following four aspects:

•1. Mixed approach implementation and problems

•2. Expected calling method

•3. Detailed implementation of inheritance library

•4. Summary

Interested friends can continue reading for details.

The earliest method I mastered to implement inheritance in js was the method of mixing prototype chain and object impersonation that I learned in xx. At work, whenever inheritance is used, I always use this method to implement it. Its implementation is simple and the idea is clear: use objects to pretend to inherit the properties of the parent class constructor, and use the prototype chain to inherit the methods of the parent class prototype object, which satisfies all inheritance scenarios I have encountered. Because of this, I never thought that the next time I write about inheritance, I have to write it in a different way. Until tonight I read a series of articles about JavaScript inheritance on Sanshengshi (it was published very early, and I just read it now. It’s really a bit Unfortunately), I discovered that in js, the inheritance mechanism can also be written so close to the implementation of back-end languages ​​​​such as java, which is indeed wonderful! So I want to fully understand the ideas of his blog and implement an inheritance library that I can use in the future.

1. Mixed approach implementation and problems

Before understanding the problem, let’s take a look at its specific implementation:

- 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
Copy after login

In terms of results, there is no problem with this inheritance implementation. The instance of Manager inherits the instance attributes and instance methods of the Employee class at the same time, and the results of the instanceOf operation are also correct. However, in terms of code organization and implementation details, this method still has the following problems:

1) The code organization is not elegant enough. The logic of the key parts of the inheritance implementation is universal and has the following structure:

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

This code lacks encapsulation. In addition, when adding an instance method of a subclass, you cannot set it through SubClass.prototype = { method1: function() {} }, otherwise the entire prototype of the subclass will be modified, and inheritance will not be possible. In this way Every time I have to write according to the structure of SubClass.prototype.method1 = function() {}, the code looks very discontinuous.

Solution: Use modularization to encapsulate common logic and provide a simple interface to the outside world. As long as you call it according to the agreed interface, you can simplify the construction and inheritance of classes. Please see the following content introduction for specific implementation. For the time being, only theoretical explanations can be provided.

2) When setting the prototype of the subclass to an instance of the parent class, new SuperClass() is called. This is a parameterless call to the parent class constructor, so the parent class must have a parameterless constructor. function. However, in JavaScript, functions cannot be overloaded, so it is impossible for the parent class to provide multiple constructors. In actual business, in most scenarios, the parent class constructor cannot have no parameters. In order to simulate the function in the only constructor Overloading can only be handled by judging arguments.length. The problem is that sometimes it is difficult to ensure that the arguments.length judgment logic will be added every time the parent class constructor is written. In this case, this approach is risky. If the logic in the constructor can be extracted and all the constructors of the class are parameterless functions, this problem will be easily solved.

Solution: Make all the constructors of the parent class and subclass parameterless, and do not write any logic in the constructor. Migrate the logic of the constructor to the init instance method, such as the Employee and The Manager example can be transformed into the following:

- 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;
};
Copy after login

Using the init method to complete the construction function can ensure that when setting the prototype of the subclass (Manager.prototype = new Employee()), the instantiation operation of the parent class will not go wrong. The only bad thing is when calling the class When using the constructor to initialize an instance, you must manually call the init method after calling the constructor to complete the actual construction logic:

- 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
Copy after login

It would be nice if the logic of this init could be placed inside the constructor, but this would violate the previously mentioned principle that constructors have no parameters and no logic. Think about it in another way. The purpose of this principle is to ensure that when instantiating the parent class as the prototype of the subclass, there will be no error in calling the constructor of the parent class. Then you can break this principle slightly and use it in the constructor of the class. Add a small amount of logic that will definitely solve the problem:

- 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;
};
Copy after login

The call result is still the same as the previous example. But there is still a small problem with this implementation. It introduces a global variable initializing. It would be great if this global variable could be introduced. This is actually easy to solve. As long as we encapsulate the construction and inheritance of classes into a module, and then Put this variable inside the module and there will be no problem.

3) When constructing a subclass, the prototype of the subclass is set to an instance of the parent class. This is not semantic. Inheritance should occur between classes, not between classes and instances. between. The reason why an instance of the parent class is used as the prototype of the subclass:

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

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

这么做了以后,当调用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();
}
Copy after login

解决方式:如果要在每个实例方法里,都能通过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
Copy after login

通过代理的方式,就解决了在在实例方法内部通过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
Copy after login

最后的两行输出了正确的实例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
});
Copy after login

从模拟的结果来看,我想要的继承库对外提供的名称只有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
})();
Copy after login

这一版核心代码在于类的构建和静态成员添加的部分,其它代码仅仅提供一些提前可以想到的赋值函数和变量(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
Copy after login

在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
})();
Copy after login

这一版关键的部分在于:

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
Copy after login

不过在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
})();
Copy after login

核心部分是:

只有当需要继承父类,且父类原型中有方法与当前的实例方法同名时,才会去对当前的实例方法添加代理。更详细的原理可以回到文章第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
Copy after login

Pay attention to these two calls:

The above are all the details of the inheritance library to be implemented in this article. In fact, what it does is to combine the solutions to the problems mentioned in the first part of this article with the calling scenarios simulated in the second part, and encapsulate them into a module It's just internal. The principles of each detail are easy to grasp as long as you understand the solutions summarized in Part 1. In the last version of the demonstration, we can also see that the inheritance library implemented in this article has fully met the needs of the simulation scenario. If there are any scenarios that require inheritance in the future, you can use the last version of the implementation to develop it. .

4. Summary

Under the guidance of the series of blogs about JavaScript inheritance on Sanshengshi, this article implements an easy-to-use inheritance library. It can be used to build object-oriented classes and inheritance relationships between classes more like Java language. I can foresee that in In my future work, this library will play a very important role in my code quality and function implementation, because in development, inherited coding ideas are still used a lot, especially when we do a lot of projects. On the one hand, I definitely want to write some public things into reusable components. On the other hand, I must meet the individual requirements of each project, so when writing components, I can’t write too rigidly, write more interfaces, and wait until the specific project is completed. Extend the unique functions of the project through inheritance and other methods, so that the components written will be more flexible and stable. In short, with this inheritance library, I feel that the code I write in the future will be much happier ~ So I hope the content of this article can also be of the same help to you. If it really helps, please give me some recommendations:)

The editor will introduce you to this much about the implementation of JavaScript inheritance. I hope it will be helpful to you!

source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template