[转]JavaScript的实例化与继承:请停止使用new关键字
JavaScript中的new关键字可以实现实例化和继承的工作,但个人认为使用new关键字并非是最佳的实践,还可以有更友好一些的实现。本文将介绍使用new关键字有什么问题,然后介绍如何对与new相关联的一系列面向对象操作进行封装,以便提供更快捷的、更易让人理解的实现方式。
传统的实例化与继承
假设我们有两个类,Class:function Class() {}
和SubClass:function SubClass(){}
,SubClass需要继承自Class。传统方法一般是按如下步骤来组织和实现的:
Class中被继承的属性和方法必须放在Class的prototype属性中
SubClass中自己的方法和属性也必须放在自己prototype属性中
SubClass的prototype对象的prototype(__proto__)属性必须指向的Class的prototype
这样一来,由于prototype链的特性,SubClass的实例便能追溯到Class的方法,从而实现继承:
new SubClass() Object.create(Class.prototype)
| |
V V
SubClass.prototype ---> { }
{ }.__proto__ ---> Class.prototype
举一个具体的例子:下面的代码中,我们做了以下几件事:
定义一个父类叫做Human
定义一个名为Man的子类继承自Human
子类继承父类的一切属性,并调用父类的构造函数,实例化这个子类
// 构造函数/基类
function Human(name) {
this.name = name;
} /*
基类的方法保存在构造函数的prototype属性中
便于子类的继承
*/
Human.prototype.say = function () {
console.log("say");
} /*
道格拉斯的object方法(等同于object.create方法)
*/
function object(o) {
var F = function () {};
F.prototype = o;
return new F();
} // 子类构造函数
function Man(name, age) {
// 调用父类的构造函数
Human.call(this, name);
// 自己的属性age
this.age = age;
} // 继承父类的方法
Man.prototype = object(Human.prototype);
Man.prototype.constructor = Man; // 实例化子类
var man = new Man("Lee", 22);
console.log(man);
// 调用父类的say方法:
man.say();
通过上面的代码可以总结出传统的实例化与继承的几个特点:
传统方法中的“类”一定是一个构造函数。
属性和方法绑定在prototype属性上,并借助prototype的特性实现继承。
通过new关键字来实例化一个对象。
为什么我会十分的肯定Object.create方法与道格拉斯的object方法是一致呢?因为在MDN上,object方法就是作为Object.create的一个Polyfill方案:
new关键字的不足之处
在《Javascript语言精粹》(Javascript: The Good Parts)中,道格拉斯认为应该避免使用new关键字:
If you forget to include the new prefix when calling a constructor function, then this will not be bound to the new object. Sadly, this will be bound to the global object, so instead of augmenting your new object, you will be clobbering global variables. That is really bad. There is no compile warning, and there is no runtime warning. (page 49)
大意是说在应该使用new的时候如果忘了new关键字,会引发一些问题。
当然了,你遗忘使用任何关键字都会引起一系列的问题。再退一步说,这个问题是完全可以避免的:
function foo()
{
// 如果忘了使用关键字,这一步骤会悄悄帮你修复这个问题
if ( !(this instanceof foo) )
return new foo(); // 构造函数的逻辑继续……
}
或者更通用的抛出异常即可
function foo()
{
if ( !(this instanceof arguments.callee) )
throw new Error("Constructor called as a function");
}
又或者按照John Resig的方案,准备一个makeClass工厂函数,把大部分的初始化功能放在一个init方法中,而非构造函数自己中:
// makeClass - By John Resig (MIT Licensed)
function makeClass(){
return function(args){
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
} else
return new arguments.callee( arguments );
};
}
在我看来,new关键字不是一个好的实践的关键原因是:
道格拉斯将这个问题描述为:
简单来说,JavaScript是一种prototypical类型语言,在创建之初,是为了迎合市场的需要,让人们觉得它和Java是类似的,才引入了new关键字。Javascript本应通过它的Prototypical特性来实现实例化和继承,但new关键字让它变得不伦不类。
把传统方法加以改造
既然new关键字不够友好,那么我们有两个办法可以解决这个问题:一是完全抛弃new关键字,二是把含有new关键字的操作封装起来,只向外提供友好的接口。下面将介绍第二种方法的实现思路,把传统方法加以改造。
我们开始构造一个最原始的基类Class
(类似于JavaScript中的Object类),并且只向外提供两个接口:
Class.extend 用于拓展子类
Class.create 用于创建实例
// 基类
function Class() {} // 将extend和create置于prototype对象中,以便子类继承
Class.prototype.extend = function () {};
Class.prototype.create = function () {}; // 为了能在基类上直接以.extend的方式进行调用
Class.extend = function (props) {
return this.prototype.extend.call(this, props);
}
extend和create的具体实现:
Class.prototype.create = function (props) {
/*
create实际上是对new的封装;
create返回的实例实际上就是new构造出的实例;
this即指向调用当前create的构造函数;
*/
var instance = new this();
/*
绑定该实例的属性
*/
for (var name in props) {
instance[name] = props[name];
}
return instance;
} Class.prototype.extend = function (props) {
/*
派生出来的新的子类
*/
var SubClass = function () {};
/*
继承父类的属性和方法,
当然前提是父类的属性都放在prototype中
而非上面create方法的“实例属性”中
*/
SubClass.prototype = Object.create(this.prototype);
// 并且添加自己的方法和属性
for (var name in props) {
SubClass.prototype[name] = props[name];
}
SubClass.prototype.constructor = SubClass; /*
介于需要以.extend的方式和.create的方式调用:
*/
SubClass.extend = SubClass.prototype.extend;
SubClass.create = SubClass.prototype.create; return SubClass;
}
仍然以Human和Man类举例使用说明:
var Human = Class.extend({
say: function () {
console.log("Hello");
}
}); var human = Human.create();
console.log(human)
human.say(); var Man = Human.extend({
walk: function () {
console.log("walk");
}
}); var man = Man.create({
name: "Lee",
age: 22
}); console.log(man);
// 调用父类方法
man.say(); man.walk();
至此,基本框架已经搭建起来,接下来继续补充功能。
我们希望把构造函数独立出来,并且统一命名为init。就好像
Backbone.js
中每一个view都有一个initialize
方法一样。这样能让初始化更灵活和标准化,甚至可以把init构造函数借出去我还想新增一个子类方法调用父类同名方法的机制,比如说在父类和子类的中都定义了一个say方法,那么只要在子类的say中调用
this.callSuper()
就能调用父类的say方法了。例如:
// 基类
var Human = Class.extend({
/*
你需要在定义类时定义构造方法init
*/
init: function () {
this.nature = "Human";
},
say: function () {
console.log("I am a human");
}
}) var Man = Human.extend({
init: function () {
this.sex = "man";
},
say: function () {
// 调用同名的父类方法
this.callSuper();
console.log("I am a man");
}
});
那么Class.create就不仅仅是new一个构造函数了:
Class.create = Class.prototype.create = function () {
/*
注意在这里我们只是实例化一个构造函数
而非最后返回的“实例”,
可以理解这个实例目前只是一个“壳”
需要init函数对这个“壳”填充属性和方法
*/
var instance = new this(); /*
如果对init有定义的话
*/
if (instance.init) {
instance.init.apply(instance, arguments);
}
return instance;
}
实现在子类方法调用父类同名方法的机制,我们可以借用John Resig的方案:
Class.extend = Class.prototype.extend = function (props) {
var SubClass = function () {};
var _super = this.prototype;
SubClass.prototype = Object.create(this.prototype);
for (var name in props) {
// 如果父类同名属性也是一个函数
if (typeof props[name] == "function"
&& typeof _super[name] == "function") {
// 重新定义用户的同名函数,把用户的函数包装起来
SubClass.prototype[name]
= (function (super_fn, fn) {
return function () { // 如果用户有自定义callSuper的话,暂存起来
var tmp = this.callSuper;
// callSuper即指向同名父类函数
this.callSuper = super_fn;
/*
callSuper即存在子类同名函数的上下文中
以this.callSuper()形式调用
*/
var ret = fn.apply(this, arguments);
this.callSuper = tmp; /*
如果用户没有自定义的callsuper方法,则delete
*/
if (!this.callSuper) {
delete this.callSuper;
} return ret;
}
})(_super[name], props[name])
} else {
// 如果是非同名属性或者方法
SubClass.prototype[name] = props[name];
} ..
} SubClass.prototype.constructor = SubClass;
}
最后给出一个完整版,并且做了一些优化:
function Class() {} Class.extend = function extend(props) { var prototype = new this();
var _super = this.prototype; for (var name in props) { if (typeof props[name] == "function"
&& typeof _super[name] == "function") { prototype[name] = (function (super_fn, fn) {
return function () {
var tmp = this.callSuper; this.callSuper = super_fn; var ret = fn.apply(this, arguments); this.callSuper = tmp; if (!this.callSuper) {
delete this.callSuper;
}
return ret;
}
})(_super[name], props[name])
} else {
prototype[name] = props[name];
}
} function Class() {} Class.prototype = prototype;
Class.prototype.constructor = Class; Class.extend = extend;
Class.create = Class.prototype.create = function () { var instance = new this(); if (instance.init) {
instance.init.apply(instance, arguments);
} return instance;
} return Class;
}
下面是测试的代码。为了验证上面代码的健壮性,故意实现了三层继承:
var Human = Class.extend({
init: function () {
this.nature = "Human";
},
say: function () {
console.log("I am a human");
}
}) var human = Human.create();
console.log(human);
human.say(); var Man = Human.extend({
init: function () {
this.callSuper();
this.sex = "man";
},
say: function () {
this.callSuper();
console.log("I am a man");
}
}); var man = Man.create();
console.log(man);
man.say(); var Person = Man.extend({
init: function () {
this.callSuper();
this.name = "lee";
},
say: function () {
this.callSuper();
console.log("I am Lee");
}
}) var person = Person.create();
console.log(person);
person.say();
是时候彻底抛弃new关键字了
如果不使用new关键字,那么我们需要转投上两节中反复使用的Object.create
来生产新的对象
假设我们有一个矩形对象:
var Rectangle = {
area: function () {
console.log(this.width * this.height);
}
};
借助Object.create,我们可以生成一个拥有它所有方法的对象:
var rectangle = Object.create(Rectangle);
生成之后,我们还可以给这个实例赋值长宽,并且取得面积值
var rect = Object.create(Rectangle);
rect.width = 5;
rect.height = 9;
rect.area();
注意这个过程我们没有使用new关键字,但是我们相当于实例化了一个对象(rectangle),给这个对象加上了自己的属性,并且成功调用了类(Rectangle)的方法。
但是我们希望能自动化赋值长宽,没问题,那就定义一个create方法:
var Rectangle = {
create: function (width, height) {
var self = Object.create(this);
self.width = width;
self.height = height;
return self;
},
area: function () {
console.log(this.width * this.height);
}
};
使用方式如下:
var rect = Rectangle.create(5, 9);
rect.area();
在纯粹使用Object.create的机制下,我们已经完全抛弃了构造函数这个概念。一切都是对象,一个类也可以是对象,这个类的实例不过是一个它自己的复制品。
下面看看如何实现继承。我们现在需要一个正方形,继承自这个长方形
var Square = Object.create(Rectangle); Square.create = function (side) {
return Rectangle.create.call(this, side, side);
}
实例化它:
var sq = Square.create(5);
sq.area();
这种做法其实和我们第一种最基本的类似
function Man(name, age) {
Human.call(this, name);
this.age = age;
}
上面的方法还是太复杂了,我们希望进一步自动化,于是我们可以写这么一个extend函数
function extend(extension) {
var hasOwnProperty = Object.hasOwnProperty;
var object = Object.create(this); for (var property in extension) {
if (hasOwnProperty.call(extension, property) || typeof object[property] === "undefined") {
object[property] = extension[property];
}
} return object;
} /*
其实上面这个方法可以直接绑定在原生的Object对象上:Object.prototype.extend
但个人不推荐这种做法
*/ var Rectangle = {
extend: extend,
create: function (width, height) {
var self = Object.create(this);
self.width = width;
self.height = height;
return self;
},
area: function () {
console.log(this.width * this.height);
}
};
这样当我们需要继承时,就可以像前几个方法一样用了
var Square = Rectangle.extend({
// 重写实例化方法
create: function (side) {
return Rectangle.create.call(this, side, side);
}
}) var s = Square.create(5);
s.area();
结束语
本文对去new关键字的方法做了一些罗列,但工作还远远没有结束,有非常多的地方值得拓展,比如:如何重新定义instance of
方法,用于判断一个对象是否是一个类的实例?如何在去new关键字的基础上继续实现多继承?希望本文的内容在这里只是抛砖引玉,能够开拓大家的思路。
引用资料
Object.create() Improves Constructor-Based Inheritance In Javascript - It Doesn't Replace It
Understanding the difference between Object.create() and new SomeFunction() in JavaScript
作者简介
李光毅,新晋前端工程师,现就职于爱奇艺,热于前端技术分享。
感谢崔康对本文的审校。
来源:
http://www.infoq.com/cn/articles/javascript-instantiation-and-inheritance
[转]JavaScript的实例化与继承:请停止使用new关键字的更多相关文章
- Javascript的实例化与继承:请停止使用new关键字
本文同时也发表在我另一篇独立博客 <Javascript的实例化与继承:请停止使用new关键字>(管理员请注意!这两个都是我自己的原创博客!不要踢出首页!不是转载!已经误会三次了!) 标题 ...
- JavaScript 面向对象 原型(prototype) 继承
1.对象的概念:无需属性的集合,属性可以为数值,对象或函数,ECMAscript中没有类的概念,这点是javascript与其他面向对象(OO)语言不同的地方. //创建一个自定义对象 var per ...
- JavaScript对寄生组合式继承的理解
有关JavaScript的几种继承方式请移步JavaScript的几种继承方式 原型链的缺陷 SubType.prototype = new SuperType(); 这样做的话,SuperType构 ...
- Javascript之对象的继承
继承是面向对象语言一个非常重要的部分.许多OOP语言都支持接口继承和实现继承两种方式.接口继承:继承方法签名:实现继承:继承实际的方法.在ECMAScript中函数是没有签名的,所以也就无法实现接口继 ...
- javascript中的原型继承
在Javascript面向对象编程中,原型继承不仅是一个重点也是一个不容易掌握的点.在本文中,我们将对Javascript中的原型继承进行一些探索. 基本形式 我们先来看下面一段代码: <cod ...
- JavaScript面向对象中的继承
1.1继承的基本概念 使用一个子类,继承另一个父类,那么子类可以自动拥有父类中的所有属性和方法,这个过程叫做继承. >>>继承的两方,发生在两个类之间. 实现继承的三种方式: 扩展O ...
- Javascript 进阶 面向对象编程 继承的一个样例
Javascript的难点就是面向对象编程,上一篇介绍了Javascript的两种继承方式:Javascript 进阶 继承.这篇使用一个样例来展示js怎样面向对象编程.以及怎样基于类实现继承. 1. ...
- Javascript 进阶 面向对象编程 继承的一个例子
Javascript的难点就是面向对象编程,上一篇介绍了Javascript的两种继承方式:Javascript 进阶 继承,这篇使用一个例子来展示js如何面向对象编程,以及如何基于类实现继承. 1. ...
- JavaScript原型链和继承
1.概念 JavaScript并不提供一个class的实现,在ES6中提供class关键字,但是这个只是一个语法糖,JavaScript仍然是基于原型的.JavaScript只有一种结构:对象.每个对 ...
随机推荐
- request 防盗链
package request; import java.io.IOException;import javax.servlet.ServletException;import javax.servl ...
- 解决php网页运行超时问题:Maximum execution time of 30 seconds exceeded
Fatal error: Maximum execution time of 30 seconds exceeded in C:\Inetpub\wwwroot\ry.php on line 11 意 ...
- StringBuilder的append、StringBuffer的append和String str = "a"+"b"的区别?
大家都知道String+String会开销额外的系统资源,粗略的原因是String是不可变类,每一步操作都会返回新的String变量,占用空间及时间. 其实我的理解不是这样的,我们来看看String+ ...
- JAVA学习第五十二课 — IO流(六)File对象
File类 用来给文件或者目录封装成对象 方便对文件与目录的属性信息进行操作 File对象能够作为參数传递给流的构造函数 一.构造函数和分隔符 public static void FileDemo( ...
- c++ 系统函数实现文件拷贝
#include "stdafx.h" #include <string> #include<windows.h> #include<iostream ...
- Android中的android:layout_width和android:width
最近在看android的东西,发现很多和web前台的东西一样(思想).只是看到很多属性的写法和前台有差别,刚刚看到这样的属性: android:width 其实是定义控件上面的文本(TextView) ...
- String知识点
- web无插件播放RTSP摄像机方案,拒绝插件,拥抱H5!
本文转自:http://www.cnblogs.com/babosa/p/7355468.html 需求 问题:有没有flash播放RTSP的播放器?H5能不能支持RTSP播放? 答案:没见过,以后估 ...
- 远程服务器上的weblogic项目管理(四)filelock not found错误解决方法
重启weblogic时如果有残余进程没有kill,启动时便可能会造成filelock not found,文件锁未找到错误,解决方法如下: 删掉Domain下的*.lok文件:(如果不熟悉文件路径推荐 ...
- Linux 下搭建 Sonatype Nexus Maven 私服
一.为什么需要搭建mave私服 如果没有私服,我们所需的所有构件都需要通过maven的中央仓库和第三方的Maven仓库下载到本地,而一个团队中的所有人都重复的从maven仓库下 载构件无疑加大了仓库的 ...