我基本從來不寫工作的事兒。
因為工作實在沒啥好寫的,不就是工作唄。
然後今天打算稍微寫一點,就寫JS吧。
我一直相信,所有的編程語言都可以分為兩部分,表象與內核。
什麼是表象?語法就是表象。
什麼是內核?去除表象以後的就是內核。
很多人學編程語言學的就是語法,但語法這貨只要記憶力足夠OK,半天就能搞定嘛。
我司有一位新人,現在已經被開除了,他說過這麼一句名言:
我會編程啊,C、C++、Java、Ruby和Python的For循環或者If語句我都會的。
一旁旁聽的我聽了只能笑笑。
這貨另一句名言是:這個和我學校裡學的不一樣啊!
大家一起呵呵。
PS:此人交大畢業,到我司的筆試聽說是滿分。
回到正題。
語法是表象,不重要——當然了,內核總會牽扯到語法怎麼實現的,所以並不是說那麼地不重要。
關鍵是拋開表象後的東西。
在我看來,JS在拋開無聊的語法表象後,真正剩下的東西是這些:
1,對象與域
對象,Object。
域,Scope。
一切變量要么隸屬於某個對象,要么隸屬於某個域——最外層的域當然就是DOM/BOM了。
比如下面這個:
var name = '???';
function classA () {
var tag = '~~~';
this.name = 'test';
this.show = function () {console.log(this.name + tag + name);};
}
var test = new classA();
這裡,最外層的對象有兩個,name和test(String也是對象)。而最外層的Scope是DOM/BOM,然後是test內的Scope。
所以以var形式生命的對象都隸屬於其所在的Scope,而所有以this.xxx形式聲明的對象都隸屬於所屬的對象(比如classA中的this.name,在實例化後name這個對象就隸屬於test這個類實例)。
Scope的最大特點,就是Scope是可以傳遞的,這也就是JS中的callback這麼強大的一個原因:
var name = 'xxx';
body.addEventListener('click', function clickHandler (evt) {
var tag = 'aaa';
$.ajax({...})
.done(function ajaxHandler (result) {
console.log('Done', name, tag);
});
});
在上面的例子中,最外層的scope被傳遞給函數clickHandler,或者更應該說是最外層scope的訪問權限被傳遞給了clickHandler的scope,於是你可以在clickHandler內訪問外層定義的變量name。
同樣的,clickHandler的scope和最外層scope被一起傳遞給ajax請求的處理函數ajaxHandler中,浴室你在這裡面也就可以訪問最外層的name和clickHandler中定義的tag了。
如果你看過V8(Google給Chrome系用的JS引擎)的源碼,你就能找到一個Scope對象,就是用來幹這事的。为V8写插件或者给NodeJS写调用V8的插件(这种一般都是C/C++写的了)时,一定要记住给予恰当的Scope,将一个底层对象放到不同的Scope中往往会得到不同的效果,因为可以访问的东西不一样了。
当然不是只有Scope可以传递,Object也可以传递,这就牵扯到了JS中的原型链,等下再说。
对于scope来说,最合理的理解是这样的:
一个对象可以隶属于多个scope。如果它隶属于多个scope,则所有scope必然满足这样一种链型关系:S1∈...∈Sn。也就是说,Sn必然是最大的scope,然后Sn中包含有Sn-1,Sn-1包含Sn-2,以此类推,知道最小的S1,这里就是某个对象所在的最小Scope。
而后,一个对象只能访问同Scope的变量,或者说只有同属一个scope的对象才能相互访问,这就是Scope内变量的访问规则。
当一个对象隶属于多个Scope时,它访问别的对象的顺序就是上面的那条链,它会先在S1中寻找指定对象是否存在,然后去S2中寻找指定对象是否存在,一直到找到指定名字的对象位置——所以说对指定名字的对象的搜索是会在链条的某处截断的,而不会每次都遍历一遍整个链条。这也就是说,包含对象的更小的Scope会“覆盖”掉更大的Scope中的同名对象。
JS中的所有一切都是对象,唯独scope不是,这是因为这货是JS引擎提供的“编制外”成员,并不是JS可以直接访问和操纵的东西——所以,我们可以用原型链来修改一个对象的父对象的方法,从而实现元编程,但如果一个对象的方法是用了类定义函数scope中的“私有”函数的话,我们就对它无能为力了,因为无法操纵scope。
JS最“神奇”的功能之一“闭包”也就是利用scope的特性——闭包不能被外界访问,这是因为scope内对象不能被外界访问,而只能被scope内对象访问。
如果我们在Chrome中打开一个页面,那么可以在console中查看一个对象所属的closjure,这是新版本Chrome所提供的功能。
Scope不能被用户在JS中有意识地创建,而只能在创建某些JS对象的时候“附带”地获得。
比如在上面的例子中,我们创建一个function,那么就获得了这个function内的scope。我们new了一个类实例,也就在这个实力内创建了一个scope——当然,这两件事其实是一件事。
又由于scope内的对象只能被scope内对象访问而不能被外部访问,所以我们就有了JS中的public和private对象的创建方法,那就是上头第一段代码中的classA这个类。
这个类被实例化后,就有了一个Object和一个Scope。在Scope中我们存放了一个对象tag,而在Object内我们存放了两个对象name和show。外界可以通过test.name来访问test这个Object的对象,但无法访问test被创建时所处scope内的对象tag,从而name和show对外界来说是public的,而tag对外界来说是private的。
这是scope的一个比较重要的用法。
我们可以比较一下JS和Ruby中Scope的差异。
Ruby中的Scope在Class、Module和Def中都是闭合掉的,比如:
my_var = "Global"
class ClassTest
my_var = "Inside Class"
name = "Test-Class"
def show
puts "My Name Is #{name} in #{my_var}"
end
end
test = ClassTest.new
test.show
这样,最后会返回错误,因为在show内无法访问my_var,也无法访问name。
这是和JS中最大的不同。要穿越这个闭合的Scope Gate,可以用如何方法:
my_var = "Global"
ClassTestA = Class.new do
name = "Test-Class"
define_method :show do
puts "My Name Is #{name} in #{my_var}"
end
end
test = ClassTest.new
test.show
这样就可以访问class外定义的my_var变量了。
可这么做也有缺点,那就是show这个函数无法被重载,一旦被重载,重载的地方将无法访问name,甚至无法访问my_var。
回到JS中。JS的Scope的传递方式前面已经介绍过了,所以如果我希望在ScopeA里访问非ScopeA的上层的ScopeB中的变量,原则上是不可能的,比如下面这个:
var tag = "global";
function show_tag () {
console.log(tag);
}
function test (callback) {
var tag = "inside";
callback();
}
test(show_tag);
上面的运行结果是global。如果我们希望show_tag访问test内的tag,这个一般是不可能的,但凡事总有例外,于是就有了这么一种很危险的突破方法:
function test (callback) {
var tag = "inside";
eval("var __callback__ = " + callback.toString() + ";__callback__();");
}
这样的运行结果就会是我们所期望的inside。
这种方法的原理其实是是用eval来动态构造函数并执行该函数。当我们使用eval来动态构造函数的时候,因为执行的是新的函数,所以原函数的scope完全无效,新函数的scope完全由构造函数的scope决定。
当然,因为使用eval来动态构造函数,所以这个方法本身也会带来很多风险。
2,原型链
先看这么一段东西:
function classA (name) {
this.name = "I'm " + name;
this.show = function () {
console.log('>> ' + this.name);
}
}
var a = new classA('A'), b = new classB('B');
a.show();
b.show();
console.log(a.__proto__ === b.__proto__);
console.log(a.show === b.show);
console.log(a.__proto__.show)
我们看到,a和b的原型(或者说父对象)是相同的,但a和b的show方法是不同的,而且a和b的父对象是没有show方法的。
接下来这么做:
a.__proto__.test = function () {
console.log('Test ' + this.name);
};
a.test();
b.test();
console.log(a.test === b.test);
console.log(b.__proto__.test);
这么一来,事情就好玩了,我们终于在a和b的原型上添加了一下好玩的东西了。
事实上,new一个类的过程其实是这样的:
function classA () {
var a = 'A';
var b = 'B';
this.x = 'X';
this.y = 'Y';
}
var testA = new classA();
function classB () {
var a = 'A';
var b = 'B';
var obj = {};
obj.x = 'X';
obj.y = 'Y';
return obj;
}
var testB = classB();
我们用new classA()创造出来的东西,和用classB()创造出来的东西,其实是一样的。
这样,回到一开始的问题:为何a.show和b.show是两个不同的函数?
原因很简单,它们是两个不同的对象a和b所独有的函数,而不是来自于某个特殊的共同祖先——原型。
可是,有的时候a和b的test函数其实做的是完全相同的事情:将对象的name属性打印出来。既然如此,如果这函数不是公用的,这就表示我有几个classA的实例,就创建了几个show函数,这对内存是极大的浪费,从而为了节约内存,我们需要这样:
function classA (name) {
this.name = name;
}
classA.prototype.show = function () {
console.log("My Name Is " + this.name);
};
这样就好了。
其实这个做法等价于:
(new classA()).__proto__.show = function () {
console.log("My Name Is " + this.name)
};
这就引出了很好玩的关于原型链的话题。
JS中的对象通过原型链来实现“继承”,而原型就是上述obj.__proto__这东西。
通过一个对象的__proto__接口,我们可以方位这个对象的“原型”。
要理解原型,先要理解类和实例。
从上面的分析可以看出,类可以被理解为构造对象的一个函数,因此JS中其实只有对象的概念,没有类的概念,虽然我们可以通过instanceof来判断一个对象是否是一个“类”的实力,但实际上这货所作的是比较一个对象的constructor属性所指的函数是否是这个对象的构造函数。
因此,JS中其实没有类和实例,有的只是构造函数和对象。
对象的原型是另一个对象,几个对象C1、C2、C3可以共有一个原型对象P。当一个对象要寻找对象属性或者方法(而非Scope属性或者方法)的时候,会先从自己开始找起,自己没有的话就去原型那找,原型也没有的话就去原型的原型那找,以此类推。
原型只能传递或者说提供“钩”在对象上的属性和方法,而不能提供Scope。
而这一套机制,就被称为原型链。
所以说JS的“继承”和别的OOP的继承是那么地不同,因为原则上说,JS根本没有类,所以也就谈不上继承了。而原型链又在表现上有点类似继承的关系,所以就让人以为JS也有“继承”。
回到一开始的问题,如果我要让show方法不被创建多次,应该怎么做?方法就是使用构造函数的prototype来给出方法。
考虑上prototype后,其实一个构造函数所作的是这么一件事:
function classA () {
this.A = 'A';
}
classA.prototype.show = function () {
return this.A;
};
var classB_prototype = {};
classB_prototype.show = function () {
return this.A;
};
function classB () {
var obj = {};
obj.A = 'A';
obj.__proto__ = classB_prototype;
return obj;
}
这么一来,我们应该就可以看清楚原型和构造函数到底是干嘛的了。
于此相关的,就是对象的this这个属性了。
这是JS引擎提供给对象的固有属性,可以认为是一个指针,决定了指向具体什么对象——到底是指向构造函数创造出来的对象,还是这个对象的原型,还是整条原型链上的某个环节。
需要注意的,就是this本身是属于scope的,this.xxx才是属于this所指向的对象的,所以在很多时候直接使用this会出各种各样的幺蛾子——特别是在使用匿名函数回调等等东东的时候,一个scope切换,就等着乐呵吧。
this的指向是可以被修改的,这就是JS里经常看到的xxx.call方法,将一个函数xxx内的this指向到指定的obj——
function ClassA (name) {
this.name = name;
}
ClassA.prototype.class = "Class A";
ClassA.prototype.show = function () {
console.log("Object " + this.name + " Of Class " + this.class);
};
var objA = new ClassA("Object A");
objA.show();
var objTmp = {
name : "No Name",
class: "No Class"
};
objA.show.call(objTmp);
运行结果为:
Object Object A Of Class Class A
Object No Name Of Class No Class
由于在复杂环境中,Scope的转移和this的转移根据不同的规则来——scope依赖于上下文context,而this依赖于原型链,所以JS才会有很多很丰富的特性——尤其是各种回调和异步方式。
我们可以ruby做一个对比。
在ruby中是的确有类这个概念的,而且,ruby的动态特性允许我们修改已经定义好的类的定义——这在js中是做不到的,我们只能修改一个对象的原型,但不能修改这个对象的类定义本身——所以JS的诸多编程模式中有一个就是在所有能用原型的地方都用到了原型,这样当我要修改类定义的时候,就把原型充当类定义来用了。这么做有好的地方,当然也有不好的地方——原型链过长的话,每次用this调用属性/方法的时候的开销也就大了。
而另一方面,ruby可以通过module和mix-in做到很好的代码重用和多重继承,这个在js里比较麻烦,因为一个js的原型只有一个——当然,这事也不是说就做不到,只不过麻烦一点:
function clsP1 () {
this.funcitonA = ...
}
function clsP2 () {
this.funcitonB = ....
}
funciton clsPMain () {
this.propertyC = ....
}
function clsChild () {
var me = this;
var p1 = new clsP1(), p2 = new clsP2();
this.functionA = function () {
p1.functionA.call(me, arguments);
};
this.functionB = function () {
p2.functionB.call(me, arguments);
};
}
clsChild.prototype = new clsPMain();
在上面的例子中,clsChild的实例的原型是clsPMain的实例,但clsChild的实例依然可以使用clsP1和clsP2的方法(而且this指向的确是它们俩)。这也是一种很常见的多重继承方案。
与此相关的一个有趣的现象,那就是JS中的对象是可以“换家长”的,而ruby就不能这么“放肆”:
var ParentA = {
show: function () {
console.log(">>>>>> " + this.name);
}
}
var ParentB = {
show: function () {
console.log(this.name + " <<<<<<");
}
}
function classClass (name) {
this.name = name;
}
classClass.prototype = ParentA;
var obj = new classClass("Lost");
obj.show();
classClass.prototype = ParentB;
obj.show();
obj.__proto__ = ParentB;
obj.show();
obj.__proto__ = ParentA;
obj.show();
像这种随便换老爹的事情,大概是JS绝无仅有的吧。。。这也是原型链和继承的最大不同。
我们事实上可以说,所谓继承,就是类的原型链,但和对象的原型链毕竟还是不同的。
Ruby虽然一样是动态语言,但Ruby毕竟还有类的概念,从而也有访问器的概念,而这个JS就没有。所以当我们在看Ruby很欢脱地private、public或者protected的时候,JS的心理总是会有一些酸楚。当然,世事无绝对,JS也可以玩访问器,不过就是麻烦点:
function isNull (obj) {
if (obj === null) return true;
if (obj === undefined) return true;
if (typeof obj === 'undefined') return true;
return false;
}
function classMap () {
var keys = [], values = [];
this.set = function (key, value) {
var index = keys.indexOf(key);
if (index === -1) {
keys.push(key);
values.push(value);
}
else {
values[index] = value;
}
};
this.get = function (key) {
var index = keys.indexOf(key);
if (index === -1) return null;
return values[index];
};
this.remove = function (key) {
var index = keys.indexOf(key);
if (index === -1) return;
keys.splice(index, 1, 0);
values.splice(index, 1, 0);
};
this.find = function (value) {
var index = values.indexOf(value);
if (index === -1) return null;
return keys[index];
};
this.each = function (callback) {
var l = keys.length, i;
for (i = 0; i < l; i++) {
callback(keys[i], values[i]);
}
};
}
var jlass = (function jlass (global) {
var obj_net = new classMap();
function clsAccessor (host) {
this.public = function (name, obj) {
if (isNull(obj)) {
return getPublic(host, name);
}
else {
return setPublic(host, name, obj);
}
}
this.private = function (name, obj) {
if (isNull(obj)) {
return getPrivate(host, name);
}
else {
return setPrivate(host, name, obj);
}
}
}
function newAccessor (host) {
var accessor = new clsAccessor(host);
checkLink(host).accessor = accessor;
return accessor;
}
function getAccessor (host) {
return checkLink(host).accessor;
}
function checkLink (obj) {
var link = obj_net.get(obj);
if (isNull(link)) {
link = {
parent: [],
accessor: null,
vPrivate: {}
};
obj_net.set(obj, link);
}
return link;
}
function setParent (child, parent, isPrototype) {
var link = checkLink(child);
if (!isNull(isPrototype) || link.parent.length <= 0) {
child.__proto__ = parent;
}
if (link.parent.indexOf(parent) < 0) {
link.parent.push(parent);
}
}
function getPublic (host, name) {
return host[name];
}
function getPrivate (host, name) {
var vars = checkLink(host);
var result = vars.vPrivate[name];
if (!isNull(result)) return result;
var parents = vars.parent, l = parents.length, i;
for (i = l - 1; i >= 0; i--) {
result = getPrivate(parents[i], name);
if (!isNull(result)) return result;
}
return null;
}
function setPublic (host, name, obj) {
host[name] = obj;
return obj;
}
function setPrivate (host, name, obj) {
var vars = checkLink(host);
vars.vPrivate[name] = obj;
return obj;
}
var jlass = function (className, structure) {
var constructor = function () {
var obj = {};
obj.extend = function (parent) {
setParent(obj, parent);
};
var args = [], l = arguments.length, i;
newAccessor(obj);
args.push(getAccessor);
for (i = 0; i < l; i++) args.push(arguments[i]);
structure.apply(obj, args);
return obj;
};
constructor.className = className;
return constructor;
};
return jlass;
}) ();
var classTest = new jlass('Class Test', function (accessor, name, age, sex) {
this.family = "Winter";
accessor(this).public('name', name);
accessor(this).private('age', age);
accessor(this).private('sex', sex);
this.sayName = function () {
console.log("Name: " + accessor(this).public('name'));
};
this.sayAge = function () {
console.log("Age : " + accessor(this).private('age'));
};
this.saySex = function () {
console.log("Sex : " + accessor(this).private('sex'));
};
});
var objTest = new classTest('Test Object', 18, true);
console.log(objTest);
objTest.sayName();
objTest.sayAge();
objTest.saySex();
console.log('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
var classChild = new jlass('Class Child', function (accessor, name, job, age) {
this.family = "Winter";
accessor(this).public('name', name);
accessor(this).private('job', job);
accessor(this).private('age', age);
this.sayJob = function () {
console.log("Job : " + accessor(this).private('job'));
};
});
var objChild = new classChild("Test Child", "RD", 20);
objChild.extend(objTest);
console.log(objChild);
objChild.sayName();
objChild.sayJob();
objChild.sayAge();
objChild.saySex();
耶~~
除此以外的JS相关的部分,个人都认为没啥特别值得记录的,无非就是熟能生巧耳,所以就只写这两个东西。
临睡前写的总结,万一哪里写错了的话,大家帮忙一起捉虫吧~~~