Javascript的实例化与继承:请停止使用new关键字

 

本文同时也发表在我另一篇独立博客 《Javascript的实例化与继承:请停止使用new关键字》(管理员请注意!这两个都是我自己的原创博客!不要踢出首页!不是转载!已经误会三次了!)

标题当然是有一点耸人听闻了,但个人觉得使用new关键字确实并非是一个最佳的实践。换句话说,我觉得有更好的实践,让实例化和继承的工作在javascript更友好一些,本文所做的工作就是教你对new关联的操作进行一系列封装,甚至完全抛弃new关键字。

在阅读本文之前你必须要对javascript中关于prototypeconstructor, 以及如何实现面向对象,this关键字的使用等概念非常熟悉,否则,相信我,你会看的非常头大。如果目前还不是很熟悉的话,可以参考我的前两篇博客Javascript: 从prototype漫谈到继承(1)Javascript: 从prototype漫谈到继承(2)。这两篇文章目前还有一些叙述有误的地方,但是还是可以提供一些参考。

传统的实例化与继承

还是先温习一下javascript继承的原理吧

假设我们有两个类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();
} // 子类Man
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);

以上我们可以总结出传统的实例化与继承的几个特点:

  • 传统方法中的“类”一定是一个构造函数——你可能会问还有可能不是构造函数吗?当然可以,文章的最后会介绍如何实现一个不是构造函数的类。
  • 属性和方法的继承一定是通过prototype实现,也一定是通过Object.create方法,也就是道格拉斯的object方法。你可能又要问了:何以见得,Object.create与object方法是一致?这当然不是我说的,而是在MDN上object是作为Object.create的一个Polyfill方案。
  • 实例化一个对象,一定是通过new关键字来实现的。(你能回忆起除了new关键字,还有其他哪些方式来创建一个对象吗?)

那么new关键字的不足之处在哪?

首先在《Javascript语言精粹》(Javascript: The Good Parts)中,道格拉斯原话是这样叙述的:

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关键字,将会非常糟糕。但我不觉得这是一个恰当的理由,或者说这个理由非常牵强。遗忘使用任何东西都会引起一系列的问题,何止于new关键字呢,再者说其实这个是有办法解决的:

function foo()
{
// if user accidentally omits the new keyword, this will
// silently correct the problem...
if ( !(this instanceof foo) )
return new foo(); // constructor logic follows...
}

或者作为一个更通用的方案,抛出异常即可

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关键字不是一个好的实践的原因是因为,

new is a remnant of the days where JavaScript accepted a Java like syntax for gaining “popularity”.

And we were pushing it as a little brother to Java, as a complementary language like Visual Basic was to C++ in Microsoft’s language families at the time.

和道格拉斯说的:

This indirection was intended to make the language seem more familiar to classically trained programmers, but failed to do that, as we can see from the very low opinion Java programmers have of JavaScript. JavaScript’s constructor pattern did not appeal to the classical crowd. It also obscured JavaScript’s true prototypal nature. As a result, there are very few programmers who know how to use the language effectively.

简单来说,javascript是一种prototypal类型语言,在创建之初,为了迎合市场的需要,为了让人们觉得它和Java是类似的,才引入了new关键字。Javascript本应通过它的Prototypical特性来实现实例化和继承,但new关键字让它变得不伦不类。想了解上面引用段落的全篇,可以参考本文最后的参考文献。

把传统方法加以改造

我们目前有两种选择,一是完全抛弃new关键字,二是把含有new关键字的操作封装起来,只向外提供友好的接口。现在我们先做第二件事,最后来做第一件事。

那么封装的接口是什么?:

  • 所有的类都派生自我们自己的一个基类Class
  • 派生出一个子类方法:Class.extend
  • 实例化一个类方法:Class.create

开始吧,先把结构搭起来:

// 基类
function Class() {} Class.prototype.extend = function () {};
Class.prototype.create = function () {}; Class.extend = function (props) {
return this.prototype.extend.call(this, props);
}

因为所有的类都能派生子类都能实例化,加上所有的类都派生自基类Class,所以我们把最关键的extendcreate方法放在Class的prototype中

接下来实现create和extend方法,解释就写在注释中了:

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中
而非上面的“实例属性”中
*/
SubClass.prototype = object(this.prototype);
for (var name in props) {
SubClass.prototype[name] = props[name];
}
SubClass.prototype.constructor = SubClass; /*
因为需要以SubClass.extend的方式调用
所以要重新赋值
*/
SubClass.extend = SubClass.prototype.extend;
SubClass.create = SubClass.prototype.create; return SubClass;
}

那么如何使用,如何对它进行测试呢,还是哪我们上面的Human和Man的例子:

var Human = Class.extend({
say: function () {
console.log("Hello");
}
}); console.log(Human.create()); var Man = Human.extend({
walk: function () {
console.log("walk");
}
})

console.log(Man.create({
    name: "Lee",
    age: 22
}));

 

进行再次改造

上面的例子还有两个不足之处。

一是我们需要一个独立的初始化实例的函数,比如说叫做init。其实构造函数自己不就是一个初始化函数嘛?对,但如果有一个正式的构造函数会更能满足我们的某些需求,比如我们new一个构造函数,但是我们不想要它的实例,只想要实例上的prototype方法。这种情况就不必调用它的init函数。又或者这个init函数可以“借给”其他类使用

不足之二是我们一个类需要能调用父类方法的机制,比如在子类的同名函数中吼一声this.callSuper,就能调用父类的同名方法。

开始吧

首先在派生一个类时,你需要定义一个初始化函数init,比如

// 基类
var Human = Class.extend({
init: function () {
this.nature = "Human";
},
say: function () {
console.log("I am a human");
}
})

然后Class.create就可以改造为

// 做了一点优化
Class.create = Class.prototype.create = function () {
/*
注意在这里我们只是实例化一个构造函数
而非进行真正的“实例”
*/
var instance = new this(); /*
这是我们即将做的,调用父类的构造函数
*/
if (instance.__callSuper) {
instance.__callSuper();
} /*
如果对init有定义的话
*/
if (instance.init) {
instance.init.apply(instance, arguments);
}
return instance;
}

注意上面的instance.__callSuper(),我们就靠这条语句来实现调用父类的构造函数,那么如何实现呢?具体解释都注释中

Class.extend = Class.prototype.extend = function () {
var SubClass = function () {};
var _super = this.prototype; ... // 前提是父类拥有init函数,才能召唤
if (_super.init) {
// 定义__callSuper方法
SubClass.prototype.__callSuper = function () {
/*
有一种可能是,用户已经定义了__callSuper方法,
所以我们需要把用户自己定义的方法暂存起来,
以便以后还原 因为在下一步,我们可能需要覆盖这一个方法
*/
var tmp = SubClass.prototype.__callSuper;
if (_super._callSuper) {
SubClass.prototype.__callSuper = _super.__callSuper;
}
/*
注意,上面一步非常关键。
上面这一步处理的情况是,
当有三层或者三层以上的继承时,
可能会出现子类调用父类的init,
父类又调用祖父的init 那么
首先保证父类_super.init使用的上下文是子类的,
(因为init中添加的各个属性应该是最后添加在子类上)
就是下面的_super.init.apply(this, arguments); 再保证父类_super.init中调用的callSuper
(如果存在的话)
是父类的callSuper,而不是子类的callSuper
因为父类调用父类的callSuper是也会
是this.__callSuper的方式调用,
那么此时的this应该是指向子类的,
而this._callSuper调用的是子类的init,
这样就成了一个死循环 子类调用子类的init,__callSuper
所以此处要及时修改上下文 如果你觉得比较绕的话
你可以直接使用
if (_super.init) {
SubClass.prototype.callSuper = _super.init;
}
在三层以上的继承试试,就会出现问题了
*/ _super.init.apply(this, arguments); // 还原用户定义的方法
SubClass.prototype.__callSuper = tmp;
}
} ...
}

最后,我们还需要一个在子类方法调用父类同名方法的机制,我们可以借用John Resig的实现方法,其实和上面是一个思想,先看看怎么使用:

var Man = Human.extend({
init: function () {
this.sex = "man";
},
say: function () {
// 调用同名的父类方法
this.callSuper();
console.log("I am a man");
}
});

实现方式

Class.extend = Class.prototype.extend = function (props) {
var SubClass = function () {};
var _super = this.prototype; SubClass.prototype = object(this.prototype);
for (var name in props) {
// 如果父类同名属性也是一个function
if (typeof props[name] == "function"
&& typeof _super[name] == "function") { SubClass.prototype[name]
= (function (super_fn, fn) {
// 返回一个新的函数,把用户函数包装起来
return function () {
/*
callSuper是动态生成的,
只有当用户调用同名方法时才会生成
*/
// 把用户自定义的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;
}
  • 我并不赞同一般方法中的this.callSuper机制,从上面实现的代码来看效率是非常低的。每一次生成实例都需要遍历,与父类方法进行比较。在每一次调用同名方法是,也是要做一些列的操作。更重要的是在传统的面向对象语言中,如C++,Java,子类的同名方法应该是覆盖父类的同名方法的。何来调用父类同名方法之说?我在这里给出的是一种选择,毕竟技术是为业务需求服务的。如果真的有这么一个需求那么也无可厚非。
  • 但是我赞成在init函数中的callSuper机制,在传统的面向对象语言中,父类拥有的属性子类不是默认就应该有的吗?这也是继承的意义之一吧

最后我们给出一个完整吧,并不仅仅是完整版,而且是升级版噢,哪里升级了呢?看代码吧:

function Class() {}

Class.extend = function extend(props) {

    var prototype = new this();
var _super = this.prototype; if (_super.init) {
prototype.__callSuper = function () {
var tmp = prototype.__callSuper;
if (_super.__callSuper) {
prototype.__callSuper = _super.__callSuper;
} _super.init.apply(this, arguments);
prototype.__callSuper = tmp;
}
} 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 = function () { var instance = new this(); if (arguments.callSuper && instance.__callSuper) {
instance.__callSuper();
} 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.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.name = "lee";
},
say: function () {
this.callSuper();
console.log("I am Lee");
}
}) var p = Person.create();
console.log(p);
p.say();

真的要抛弃new关键字了

无论如何上面的方法我们都使用了new关键字,接下来叙述的是真正不是用new关键字的方法

第一个问题是:如何生成一个对象?

  • var obj = {};
  • var obj = new Fn();
  • var obj = Object.create(null)

第一个方法可拓展性太低,第二个方法我们已经决定抛弃了,那重点就在第三个方法

你们还记得第三个方法是怎么用的吗?在MDN中是这样解释的

Creates a new object with the specified prototype object and properties.

假设我们有一个矩形对象:

var Rectangle = {
area: function () {
console.log(this.width * this.height);
}
};

我们想生成一个有它所有方法的对象应该怎么办?

var rectangle =Object.create(Rectangle);

生成之后,我们还可以给这个实例赋值长宽,并且取得面积值

var rect = Object.create(Rectangle);
rect.width = 5;
rect.height = 9;
rect.area();

这是一个很神奇的过程,我们没有使用new关键字,但是我们实例化了一个对象,给这个对象加上了自己的属性,并且成功调用了类的方法。

但是我们希望能自动化赋值长宽,没问题,那就定义一个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;
} /*
其实上面这个方法可以直接写成prototype方法:Object.prototype.extend
但这样盲目的修改原生对象的prototype属性是大忌
于是还是分开来写了
*/ 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();

OK,今天的课就到这里了。其实还有很多工作可以做,比如实现多继承(Mixin模式),如何实现自定义的instancef方法等等。这篇文章算抛砖引玉吧,有兴趣的朋友可以继续研究下去。

引用文献

 
 
分类: javascript

new关键字的更多相关文章

  1. 作为一个新手的Oracle(DBA)学习笔记【转】

    一.Oracle的使用 1).启动 *DQL:数据查询语言 *DML:数据操作语言 *DDL:数据定义语言 DCL:数据控制语言 TPL:事务处理语言 CCL:指针控制语言 1.登录 Win+R—cm ...

  2. JavaScript var关键字、变量的状态、异常处理、命名规范等介绍

    本篇主要介绍var关键字.变量的undefined和null状态.异常处理.命名规范. 目录 1. var 关键字:介绍var关键字的使用. 2. 变量的状态:介绍变量的未定义.已定义未赋值.已定义已 ...

  3. java面向对象中的关键字

    1,super关键字 super:父类的意思 1. super.属性名 (调用父类的属性) 2. super.方法名 (调用父类的方法) 3. super([参数列表])(调用父类的构造方法) 注意: ...

  4. 关于javascript中的this关键字

    this是非常强大的一个关键字,但是如果你不了解它,可能很难正确的使用它. 下面我解释一下如果在事件处理中使用this. 首先我们讨论一下下面这个函数中的this关联到什么. function doS ...

  5. transient关键字的用法

    本篇博客转自 一直在路上 Java transient关键字使用小记 1. transient的作用及使用方法 我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,Java ...

  6. Java关键字:static

    通常,当创建类时,就是在描述那个类的外观和行为.只有用new创建类的对象时,才分配数据存储空间,方法才能被调用.但往往我们会有下面两种需求: 1.我想要这样一个存储空间:不管创建多少对象,无论是不创建 ...

  7. Core Java 总结(关键字,特性问题)

    2016-10-19 说说&和&&的区别 初级问题,但是还是加入了笔记,因为得满分不容易. &和&&都可以用作逻辑与的运算(两边是boolean类型), ...

  8. Net中的常见的关键字

    Net中的关键字有很多,我们最常见的就有new.base.this.using.class.struct.abstract.interface.is.as等等.有很多的,在这里就介绍大家常见的,并且有 ...

  9. php多关键字查询

      php单一关键字查询 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 tdansitional//EN" "http: ...

  10. Keil> 编译器特有的功能 > 关键字和运算符 > __weak

    __weak 此关键字指示编译器弱导出符号. 可以将 __weak 关键字应用于函数和变量声明以及函数定义. 用法 函数和变量声明 对于声明,此存储类指定一个 extern 对象声明,即使不存在,也不 ...

随机推荐

  1. 【百度地图API】——如何让标注自动呈现在最佳视野内

    原文:[百度地图API]--如何让标注自动呈现在最佳视野内 摘要: “我有一堆标注,不规则的散落在地图的各个地方,我想把它们展示在一个最佳视野中,怎么办呢?”一位API爱好者咨询道. -------- ...

  2. bootstrap-wysiwyg 结合 base64 解码 .net bbs 图片操作类 (三) 图片裁剪

    官方的例子 是 长方形的. 我这里 用于 正方形的头像 所以  做如下  修改 #preview-pane .preview-container { width: 73px; height: 73px ...

  3. SSAS系列——【02】多维数据(维度对象)

    原文:SSAS系列——[02]多维数据(维度对象) 1.维度是什么? 数学中叫参数,物理学中是独立的时空坐标的数目.0维是一点,1维是线,2维是一个长和宽(或曲线)面积,3维是2维加上高度形成体积面. ...

  4. IOS程序启动的过程

    IOS程序启动按照以下5个步骤执行 1.main函数 IOS程序启动首先执行main函数 2.UIApplicationMain 执行main函数中的UIApplicationMain函数,这个函数会 ...

  5. Facebook Hack 语言 简介

    1. Hack 是什么? Hack 是一种基于HHVM(HipHop VM 是Facebook推出的用来执行PHP代码的虚拟机,它是一个PHP的JIT编译器,同时具有产生快速代码和即时编译的优点.)的 ...

  6. 多个Storyboard切换

    - (void)showStoryboard { // 实例化MainStoryboard UIStoryboard *storyboard = [UIStoryboard storyboardWit ...

  7. HTML页面的动画的制作及性能

    原文:HTML页面的动画的制作及性能 WEB页面的动画的制作及性能 简介 目前WEB页面做动画的方式大的分两种1.JS间隔时间不断修改元素属性值,这也是CSS3出来前常用的做法,貌似也是唯一的做法.2 ...

  8. API接口开发简述示例

    作为最流行的服务端语言PHP(PHP: Hypertext Preprocessor),在开发API方面,是很简单且极具优势的.API(Application Programming Interfac ...

  9. A在SP.NET跨页多选

    在ASP.NET跨页多选 本文介绍怎样在ASP.NET中实现多页面选择的问题.其详细思路非常easy:用隐藏的INPUT记住每次选择的项目,在进行数据绑定时.检查保存的值,再在DataGrid中进行选 ...

  10. windows下使用pthread

    有的时候需要使用多线程来测试代码啥的,在Linux下有pthread,在windows也有. 我这儿是使用MingW作为编译工具,具体如何下载MingW自行在网上搜索. 而pthread是在这里下载的 ...