我基本從來不寫工作的事兒倦青。
因為工作實在沒啥好寫的瓮床,不就是工作唄。
然後今天打算稍微寫一點产镐,就寫JS吧隘庄。
我一直相信,所有的編程語言都可以分為兩部分癣亚,表象與內核丑掺。
什麼是表象?語法就是表象述雾。
什麼是內核街州?去除表象以後的就是內核。
很多人學編程語言學的就是語法绰咽,但語法這貨只要記憶力足夠OK菇肃,半天就能搞定嘛地粪。
我司有一位新人取募,現(xiàn)在已經被開除了,他說過這麼一句名言:
我會編程啊蟆技,C玩敏、C++、Java质礼、Ruby和Python的For循環(huán)或者If語句我都會的旺聚。
一旁旁聽的我聽了只能笑笑。
這貨另一句名言是:這個和我學校裡學的不一樣翱艚丁砰粹!
大家一起呵呵。
PS:此人交大畢業(yè)造挽,到我司的筆試聽說是滿分碱璃。
回到正題。
語法是表象饭入,不重要——當然了嵌器,內核總會牽扯到語法怎麼實現(xiàn)的,所以並不是說那麼地不重要谐丢。
關鍵是拋開表象後的東西爽航。
在我看來蚓让,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被傳遞給函數(shù)clickHandler紫岩,或者更應該說是最外層scope的訪問權限被傳遞給了clickHandler的scope规惰,於是你可以在clickHandler內訪問外層定義的變量name。
同樣的泉蝌,clickHandler的scope和最外層scope被一起傳遞給ajax請求的處理函數(shù)ajaxHandler中歇万,浴室你在這裡面也就可以訪問最外層的name和clickHandler中定義的tag了。
如果你看過V8(Google給Chrome系用的JS引擎)的源碼勋陪,你就能找到一個Scope對象贪磺,就是用來幹這事的。為V8寫插件或者給NodeJS寫調用V8的插件(這種一般都是C/C++寫的了)時诅愚,一定要記住給予恰當?shù)腟cope寒锚,將一個底層對象放到不同的Scope中往往會得到不同的效果,因為可以訪問的東西不一樣了违孝。
當然不是只有Scope可以傳遞刹前,Object也可以傳遞,這就牽扯到了JS中的原型鏈等浊,等下再說腮郊。
對于scope來說,最合理的理解是這樣的:
一個對象可以隸屬于多個scope筹燕。如果它隸屬于多個scope轧飞,則所有scope必然滿足這樣一種鏈型關系:S1∈...∈Sn衅鹿。也就是說,Sn必然是最大的scope过咬,然后Sn中包含有Sn-1大渤,Sn-1包含Sn-2,以此類推掸绞,知道最小的S1泵三,這里就是某個對象所在的最小Scope。
而后衔掸,一個對象只能訪問同Scope的變量烫幕,或者說只有同屬一個scope的對象才能相互訪問,這就是Scope內變量的訪問規(guī)則敞映。
當一個對象隸屬于多個Scope時较曼,它訪問別的對象的順序就是上面的那條鏈,它會先在S1中尋找指定對象是否存在振愿,然后去S2中尋找指定對象是否存在捷犹,一直到找到指定名字的對象位置——所以說對指定名字的對象的搜索是會在鏈條的某處截斷的,而不會每次都遍歷一遍整個鏈條冕末。這也就是說萍歉,包含對象的更小的Scope會“覆蓋”掉更大的Scope中的同名對象。
JS中的所有一切都是對象档桃,唯獨scope不是枪孩,這是因為這貨是JS引擎提供的“編制外”成員,并不是JS可以直接訪問和操縱的東西——所以胳蛮,我們可以用原型鏈來修改一個對象的父對象的方法销凑,從而實現(xiàn)元編程丛晌,但如果一個對象的方法是用了類定義函數(shù)scope中的“私有”函數(shù)的話仅炊,我們就對它無能為力了,因為無法操縱scope澎蛛。
JS最“神奇”的功能之一“閉包”也就是利用scope的特性——閉包不能被外界訪問抚垄,這是因為scope內對象不能被外界訪問,而只能被scope內對象訪問谋逻。
如果我們在Chrome中打開一個頁面呆馁,那么可以在console中查看一個對象所屬的closjure,這是新版本Chrome所提供的功能毁兆。
Scope不能被用戶在JS中有意識地創(chuàng)建浙滤,而只能在創(chuàng)建某些JS對象的時候“附帶”地獲得。
比如在上面的例子中气堕,我們創(chuàng)建一個function纺腊,那么就獲得了這個function內的scope畔咧。我們new了一個類實例,也就在這個實力內創(chuàng)建了一個scope——當然揖膜,這兩件事其實是一件事誓沸。
又由于scope內的對象只能被scope內對象訪問而不能被外部訪問,所以我們就有了JS中的public和private對象的創(chuàng)建方法壹粟,那就是上頭第一段代碼中的classA這個類拜隧。
這個類被實例化后,就有了一個Object和一個Scope趁仙。在Scope中我們存放了一個對象tag洪添,而在Object內我們存放了兩個對象name和show。外界可以通過test.name來訪問test這個Object的對象雀费,但無法訪問test被創(chuàng)建時所處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這個函數(shù)無法被重載菩暗,一旦被重載,重載的地方將無法訪問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來動態(tài)構造函數(shù)并執(zhí)行該函數(shù)漱办。當我們使用eval來動態(tài)構造函數(shù)的時候,因為執(zhí)行的是新的函數(shù)婉烟,所以原函數(shù)的scope完全無效娩井,新函數(shù)的scope完全由構造函數(shù)的scope決定。
當然似袁,因為使用eval來動態(tài)構造函數(shù)洞辣,所以這個方法本身也會帶來很多風險。
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()創(chuàng)造出來的東西季眷,和用classB()創(chuàng)造出來的東西余蟹,其實是一樣的。
這樣子刮,回到一開始的問題:為何a.show和b.show是兩個不同的函數(shù)威酒?
原因很簡單,它們是兩個不同的對象a和b所獨有的函數(shù)挺峡,而不是來自于某個特殊的共同祖先——原型葵孤。
可是,有的時候a和b的test函數(shù)其實做的是完全相同的事情:將對象的name屬性打印出來橱赠。既然如此尤仍,如果這函數(shù)不是公用的,這就表示我有幾個classA的實例病线,就創(chuàng)建了幾個show函數(shù)吓著,這對內存是極大的浪費,從而為了節(jié)約內存送挑,我們需要這樣:
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中的對象通過原型鏈來實現(xiàn)“繼承”惕耕,而原型就是上述obj.__proto__這東西。
通過一個對象的__proto__接口诫肠,我們可以方位這個對象的“原型”司澎。
要理解原型欺缘,先要理解類和實例。
從上面的分析可以看出挤安,類可以被理解為構造對象的一個函數(shù)谚殊,因此JS中其實只有對象的概念,沒有類的概念蛤铜,雖然我們可以通過instanceof來判斷一個對象是否是一個“類”的實力嫩絮,但實際上這貨所作的是比較一個對象的constructor屬性所指的函數(shù)是否是這個對象的構造函數(shù)。
因此围肥,JS中其實沒有類和實例剿干,有的只是構造函數(shù)和對象。
對象的原型是另一個對象穆刻,幾個對象C1置尔、C2、C3可以共有一個原型對象P氢伟。當一個對象要尋找對象屬性或者方法(而非Scope屬性或者方法)的時候榜轿,會先從自己開始找起,自己沒有的話就去原型那找朵锣,原型也沒有的話就去原型的原型那找差导,以此類推。
原型只能傳遞或者說提供“鉤”在對象上的屬性和方法猪勇,而不能提供Scope设褐。
而這一套機制,就被稱為原型鏈泣刹。
所以說JS的“繼承”和別的OOP的繼承是那么地不同助析,因為原則上說,JS根本沒有類椅您,所以也就談不上繼承了外冀。而原型鏈又在表現(xiàn)上有點類似繼承的關系,所以就讓人以為JS也有“繼承”掀泳。
回到一開始的問題雪隧,如果我要讓show方法不被創(chuàng)建多次,應該怎么做员舵?方法就是使用構造函數(shù)的prototype來給出方法脑沿。
考慮上prototype后,其實一個構造函數(shù)所作的是這么一件事:
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;
}
這么一來马僻,我們應該就可以看清楚原型和構造函數(shù)到底是干嘛的了庄拇。
于此相關的,就是對象的this這個屬性了。
這是JS引擎提供給對象的固有屬性措近,可以認為是一個指針溶弟,決定了指向具體什么對象——到底是指向構造函數(shù)創(chuàng)造出來的對象,還是這個對象的原型瞭郑,還是整條原型鏈上的某個環(huán)節(jié)辜御。
需要注意的,就是this本身是屬于scope的屈张,this.xxx才是屬于this所指向的對象的擒权,所以在很多時候直接使用this會出各種各樣的幺蛾子——特別是在使用匿名函數(shù)回調等等東東的時候,一個scope切換袜茧,就等著樂呵吧菜拓。
this的指向是可以被修改的,這就是JS里經车严茫看到的xxx.call方法纳鼎,將一個函數(shù)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
由于在復雜環(huán)境中,Scope的轉移和this的轉移根據(jù)不同的規(guī)則來——scope依賴于上下文context裳凸,而this依賴于原型鏈贱鄙,所以JS才會有很多很豐富的特性——尤其是各種回調和異步方式。
我們可以ruby做一個對比姨谷。
在ruby中是的確有類這個概念的逗宁,而且,ruby的動態(tài)特性允許我們修改已經定義好的類的定義——這在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指向的確是它們倆)斋荞。這也是一種很常見的多重繼承方案荞雏。
與此相關的一個有趣的現(xiàn)象,那就是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雖然一樣是動態(tài)語言楚昭,但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相關的部分,個人都認為沒啥特別值得記錄的捞稿,無非就是熟能生巧耳又谋,所以就只寫這兩個東西。
臨睡前寫的總結娱局,萬一哪里寫錯了的話彰亥,大家?guī)兔σ黄鹱较x吧~~~