发布于 2016-04-03 16:59:00 | 108 次阅读 | 评论: 0 | 来源: 分享
这里有新鲜出炉的Javascript教程,程序狗速度看过来!
JavaScript客户端脚本语言
Javascript 是一种由Netscape的LiveScript发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
以上这几点注意事项可以看作是在使用前面的继承库的时候应该遵守的规范。只有这样,团队所有人写的代码才能保持一致,整体健壮性才会更好。
在该书第6章-继承部分,介绍了10多种继承方法,这些方法都很简单,但是都不能作为完整的继承实现,每个方法对应的实际只是继承的单个知识点,比如原型链方法仅仅是在说明父类实例作为子类原型的这个知识点:
构造器借用法仅仅是在说明在子类构造函数内通过借用父类构造函数来继承父类实例属性的知识点:
所以本文不会逐个去介绍这每个方法的细节,因为在上文《详解Javascript的继承实现》的内容中已经把继承的大部分要点说的很详细了,而且这书中有些方法不具备广泛适用性,从我的角度来说,了解下就够了,反正我工作不会用。
本文这个部分要说明的是该书对继承方法的分类,它把javascript的继承分为了基于构造函数工作模式的继承和基于实例工作模式的继承,前者是指继承关系发生在类与类之间的方式,后者是指继承关系发生在实例与实例之间的方式,这个分类为我们带来了除了前面的继承库提供的方式之外的另外一种继承思路,而且这个思路早已经被我们在js中广泛的使用。
前文的继承库是一种基于构造函数模式的继承方式,我们在使用的时候,都是预先构建好类以及类与类的继承关系,通过类之间的扩展,来给子类实例增加父类实例不曾拥有的能力,这种方式用起来更符合编程语言对于现实世界的抽象,所以很容易理解和使用。但是有很多时候这种传统的构建和继承方式也会给我们的工作带来不少麻烦。
首先来看基于前文的继承库,我们如何实现一个单例的组件:
var Util = Class({
instanceMembers: {
trim: function(s){
s += '';
return s.replace(/s*|s*/gi, '');
}
}
});
var UtilProxy = (function() {
var ins = null;
return {
getIns: function() {
!ins & (ins = new Util());
return ins;
}
}
})();
console.log(UtilProxy.getIns() === UtilProxy.getIns());//true
按照继承库的思路,为了实现这个单例,我们一定得先定义一个组件类Util,然后为了保证这个类对外提供的始终是一个实例,还得考虑使用代理来实现一个单例模式,最后给外部调用的时候,还得通过代理去获取组件的唯一实例才行,这种做法有4个不好的地方:
一是复杂,上面看起来还比较简单,那是因为这个例子简单,要真是复杂的单例模块,要写的代码多多了;
二是麻烦,无论什么单例组件都得按这个结构写和用,要写的代码多;
三是不安全,万一不小心直接通过组件的构造函数去实例化了,单例就不无法保证了;
四是不好扩展,想想如果要扩展一个单例组件,是不是得先实现一个继承Util的类,再给这个新类实现一个代理才行:
var LOG_LEVEL_CONFIG = 'DEBUG';
var UtilSub = Class({
instanceMembers: {
log: function(info) {
LOG_LEVEL_CONFIG === 'DEBUG' & console.dir(info);
}
},
extend: Util
});
var UtilSubProxy = (function() {
var ins = null;
return {
getIns: function() {
!ins & (ins = new UtilSub());
return ins;
}
}
})();
console.log(UtilSubProxy.getIns() === UtilSubProxy.getIns());//true
所以你看,这种完全面向对象的做法在这种单例的组件需求下,就不是那么好用。所幸的是,从我们自己的工作经验来看,假如我们需要单例组件的时候,我们一般首先想到的方法都不是这种基于类的构建方法,因为javascript是一门基于对象的语言,我们在构建组件的时候,完全可以抛弃掉组件类,直接构建组件的对象,我们只关注对象的行为和特性,但是它属于哪个类别,对我们的需求而言不重要。以经验来说,通常我们有2种方式来实现这种基于对象的构建思路。第一种是直接通过对象字面量来创建实例:
var util = {
trim: function(s){
s += '';
return s.replace(/s*|s*/gi, '');
}
}
第二种,是通过立即调用的匿名函数来返回实例,实例的创建逻辑被包装在匿名函数内部,对外只提供调用接口,这种对于想要实现一些私有逻辑和逻辑封装的需求就特别方便:
var util = (function () {
var LOG_LEVEL_CONFIG = 'DEBUG';
return {
LOG_LEVEL_CONFIG: LOG_LEVEL_CONFIG,
trim: function (s) {
s += '';
return s.replace(/s*|s*/gi, '');
},
log: function (info) {
LOG_LEVEL_CONFIG === 'DEBUG' & console.dir(info);
}
}
})();
对比前面的基于类的构建方法,这两种方法完全没有前面方法提出的麻烦,复杂和不安全问题,唯一值得讨论的是第四点:这种基于对象的构建方法,好不好继承。到目前为止,还没有讨论过这种基于对象的构建,在需要扩展组件对象的功能时,该如何来实现继承或者说扩展,有没有类似继承库这种的通用机制,以便我们能够快速地基于对象进行继承。这个问题的解决方法,正是我们前面提到的《JavaScript面向对象编程指南》这本书为我们带来的另外一种思路,也就是基于实例工作模式的继承方式,它为我们提供了2种比较实用的基于对象实例的组件在继承时可以采用的方法:
1)浅拷贝模式
当我们只想为原来的组件对象添加一些新的行为的时候,我们首先想到的肯定就是下面这种方法:
//util.js
var util = {
trim: function (s) {
s += '';
return s.replace(/s*|s*/gi, '');
}
};
//other.js
util.getQueryObject = function getQueryObject(url) {
url = url == null ? window.location.href : url;
var search = url.substring(url.lastIndexOf("?") + 1);
var obj = {};
var reg = /([^?&=]+)=([^?&=]*)/g;
search.replace(reg, function (rs, $1, $2) {
var name = decodeURIComponent($1);
var val = decodeURIComponent($2);
val = String(val);
obj[name] = val;
return rs;
});
return obj;
};
直接基于原来的对象添加新的方法即可。不好的是,当我们要一次性添加多个方法的时候,这些赋值的逻辑都是重复的,而且会使我们的代码看起来很不整洁,所以可以把这个赋值的逻辑封装成一个函数,新的行为都通过newProps传递进来,由该函数完成各个属性赋值给sourceObj(原来的对象)的操作,比如以下示例中的copy函数就是用来完成这个功能的:
//util.js
var util = {
trim: function (s) {
s += '';
return s.replace(/s*|s*/gi, '');
}
};
var copy = function (sourceObj, newProps) {
if (typeof sourceObj !== 'object') return;
newProps = typeof newProps === 'object' & newProps || {};
for (var i in newProps) {
sourceObj[i] = newProps[i];
}
};
//other.js
copy(util, {
getQueryObject: function getQueryObject(url) {
url = url == null ? window.location.href : url;
var search = url.substring(url.lastIndexOf("?") + 1);
var obj = {};
var reg = /([^?&=]+)=([^?&=]*)/g;
search.replace(reg, function (rs, $1, $2) {
var name = decodeURIComponent($1);
var val = decodeURIComponent($2);
val = String(val);
obj[name] = val;
return rs;
});
return obj;
}
});
这个copy函数也就是那本书中所介绍的浅拷贝模式。有了这个copy函数,我们在工作中就能很方便地基于已有的对象进行新功能的扩展,不用再写前面提到重复赋值逻辑。不过它有一个小问题,在开发的时候值得十分注意,由于这个模式直接把newProps里面的属性值赋给sourceObj,所以当newProps里面的某个属性是一个引用类型的值时,尤其是指向数组和其它非函数型的object对象时,很容易出现引用的问题,也就是改变了newProps,同样会影响到sourceObj的问题,如果这种意外地修改并不是你所期望的,那么就不能考虑使用这种模式来扩展。不过很多时候,浅拷贝的模式也足够用了,只要你确定当你使用浅拷贝方法的时候,不会发生引用问题即可。
2)深拷贝模式
上面的浅拷贝模式存在的问题,可以用深拷贝模式来解决,与浅拷贝模式不同的是,深拷贝模式在扩展对象的时候,如果发现newProps里面的属性是一个数组或者非函数类型的object对象,就会创建一个新数组或新的object对象来存放要扩展的属性的内容,并且会递归做这样的处理,保证sourceObj不会再与newProps有相同的指向数组或者非函数类型object对象的引用。只要对前面的copy函数稍加改造,就能得到我们所需要的深拷贝模式的继承实现,也就是下面的deepCopy函数:
var deepCopy = function (sourceObj, newProps) {
if (typeof sourceObj !== 'object') return;
newProps = typeof newProps === 'object' & newProps || {};
for (var i in newProps) {
if (typeof newProps[i] === 'object') {
sourceObj[i] = Object.prototype.toString.apply(newProps[i]) === '[object Array]' ? [] : {};
copy(sourceObj[i], newProps[i]);
} else {
sourceObj[i] = newProps[i];
}
}
};
var util = {};
var newProps = {
cache: [{name: 'jason'}]
};
deepCopy(util, newProps);
console.log(util.cache === newProps.cache);//false
console.log(util.cache[0] === newProps.cache[0]);//false
有了这个deepCopy函数,浅拷贝模式的引用问题也就迎刃而解了。不过还有一点值得一说的是,由于函数在js里面也是对象,所以函数类型的数据也会存在引用问题,但是不管是深拷贝还是浅拷贝,都没有考虑这一点,毕竟函数在绝大部分场景来说,本身就属于共享型对象,就是应该重用的,所以没有必要做拷贝。
以上就是基于实例工作模式的2种继承方法实现:浅拷贝和深拷贝。关于这两种实现还有两点需要说明:
1)在实现过程中,遍历newProps的时候,始终没有用到hasOwnProperty去判断,因为hasOwnProperty是用来判断某个属性是否从该对象的原型链继承而来,如果加了这个判断,那么就会把newProps对象上的那些从原型链继承而来的属性或者方法都过滤掉,这不一定符合我们的期望,因为这两种拷贝的模式,都是基于对象来工作的,大部分场景中,在扩展一个对象的时候,我们往往是考虑把要扩展的对象也就是newProps上的所有属性和行为都添加给原来的对象,所以就不能用hasOwnProperty去判断。
2)浅拷贝的实现还算比较完整,因为它适用的范围简单。但是深拷贝的实现还不够完美,第一是可能考虑的情况不全,第二是欠缺优化。另外这两个实现的代码要是能够整合到一块,形成一个类似继承库一样的模块的话,在实际工作中才会更大的应用价值。好在jquery中已经有一个extend方法把我提到的这些问题都解决了,这也就是为啥我前面说我们已经在代码中广泛引用基于对象进行扩展这种继承思路的原因。所以在实际工作过程中,我们完全可以拿jquery.extend来实现我们基于对象的扩展,即使是不想使用jquery的环境,也可以完全拿它的extend源码实现一个能独立运行的extend模块出来,比如这样子,用法还与jQuery.extend一致:
var extend = function () {
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
if (typeof target === "boolean") {
deep = target;
// Skip the boolean and the target
target = arguments[i] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
if (typeof target !== "object" & !jQuery.isFunction(target)) {
target = {};
}
// return if only one argument is passed
if (i === length) {
return;
}
for (; i ) {
// Only deal with non-null/undefined values
if ((options = arguments[i]) != null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target === copy) {
continue;
}
// Recurse if we're merging plain objects or arrays
if (deep & copy && ( Object.prototype.toString.apply(copy) === '[object Object]' ||
(copyIsArray = Object.prototype.toString.apply(copy) === '[object Array]') )) {
if (copyIsArray) {
copyIsArray = false;
clone = src & Object.prototype.toString.apply(src) === '[object Array]' ? src : [];
} else {
clone = src & Object.prototype.toString.apply(src) === '[object Object]' ? src : {};
}
// Never move original objects, clone them
target[name] = extend(deep, clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
// Return the modified object
return target;
};
本文在上文《详解Javascript的继承实现》的基础上补充了很多了内容,首先把上文的继承库实现在实际使用的一些注意事项说明了一下,然后对《javascript面向对象编程指南》这部分继承的相关的内容做了简单介绍,本文最重要的是说明基于对象扩展的继承思路,这种思路应用最广的是浅拷贝和深拷贝的模式,在javascript这种语言中都有很多实际的应用场景。比较继承库跟基于对象扩展这两种思路,发现他们的思想和实际应用完全是不矛盾的,继承库更适合可重复构建的组件,而基于对象扩展更适合不需要重复构建的组件,每种模式都有不同的价值,在实际工作中要用什么机制来开发组件,完全取决于这个组件有没有必要重复构建这样的需求,甚至有时候我们会把这两种方式结合起来使用,比如首先通过继承库来构建组件类A,然后再实例化A的对象,最后直接基于A的实例进行扩展。我觉得这两种思路结合起来,包含了javascript继承部分相关的所有核心内容,这篇文章还有上篇文章,从要点跟实现细节说明了在继承开发当中的各方面问题,所以对于那些跟我水平差不多的web开发人员来说,应该还是有不少价值,只要把这两篇文章里面的关键点都掌握了,就相当于掌握了javascript的整个继承思想,以后编写面向对象的代码,阅读别人写的组件化代码这两方面的能力都一定能提升一个层次。最后希望本文确如我所说,能给你带来一些收获。
感谢阅读:)