感謝Node.js開發(fā)指南,參考了它的附錄部分內(nèi)容。
Javascript中的作用域是通過函數(shù)來確定的,這一點(diǎn)與C
、Java
等靜態(tài)語言有一些不一樣的地方。
if (true) {
var a = 'Value';
}
console.log(a); // Value
上面的代碼片段將會(huì)輸出Value。(在瀏覽器環(huán)境中)
再來一個(gè)更加common的例子,
var a1 = 'Valve';
var foo1 = function() {
console.log(a1);
};
foo1(); // Value
var foo2 = function() {
var a1 = 'DOTA2';
console.log(a1);
}
foo2(); // DOTA2
顯然,foo1
的結(jié)果是Value,foo2
的結(jié)果是DOTA2,這應(yīng)該很容易理解。
接下來這個(gè)例子將會(huì)讓人感到迷惑,
var a1 = 'mercurial';
var foo = function() {
console.log(a1);
var a1 = 'git';
}
foo(); // undefined
此時(shí),結(jié)果將會(huì)是undefined
。
因?yàn)樵诤瘮?shù)foo內(nèi)部的a1將會(huì)覆蓋函數(shù)外部的變量a1,js搜索作用域是按照從內(nèi)到外的,而且當(dāng)執(zhí)行到console.log時(shí),函數(shù)作用域內(nèi)部的a1還尚未被初始化,所以會(huì)輸出undefined。
其實(shí)這里還涉及到一個(gè)變量懸置的概念,即在Javascript的函數(shù)中,無論在何處聲明或者初始化的變量都等效于函數(shù)的起始位置聲明,在實(shí)際位置賦值。如下,
var foo = function() {
// do something
var a = 'ok';
console.log(a);
// do something
}
上面這段代碼等效于,
var foo = function() {
var a; // 注意看這里!
// do something
a = 'ok';
console.log(a);
// do something
}
最后還有一點(diǎn)需要說明的就是,未定義變量和定義但未被初始化的變量,雖然他們的值輸出都是undefined,但是在js內(nèi)部的實(shí)現(xiàn)上還是有區(qū)別的。未定義的變量存在于js的局部語義表上,但是未被分配內(nèi)存,而定義卻未初始化的變量時(shí)實(shí)際分配了內(nèi)存的。
接下來這個(gè)例子將會(huì)演示函數(shù)作用域的嵌套,
var foo = function() {
var a1 = 'foo';
(function() {
var a1 = 'foo1';
(function() {
console.log(a1);
})();
})();
};
foo(); // foo1
輸出結(jié)果是foo1。這里我在最內(nèi)層的console.log
中打印a1,此時(shí),因?yàn)樽顑?nèi)層的作用域中沒有a1的相關(guān)定義,所以會(huì)往上層作用域搜索,得到a1=’foo1’。這里實(shí)際上有一個(gè)嵌套的作用域關(guān)系。
這里還有一點(diǎn)需要注意,就是函數(shù)作用的嵌套關(guān)系是在定義時(shí)就會(huì)確定的,而非調(diào)用的時(shí)候。也即js的作用域是靜態(tài)作用域,好像又叫詞法作用域,因?yàn)?strong>在代碼做語法分析時(shí)就確定下來了??聪旅娴倪@個(gè)例子,
var a1 = 'global';
var foo1 = function() {
console.log(a1);
};
foo1(); // global
var foo2 = function() {
var a1 = 'locale';
foo1();
};
foo2(); // global
示例的輸出結(jié)果都將會(huì)是global
。foo1()
的執(zhí)行結(jié)果為global
不需要太多的解釋,很容易明白。
因?yàn)?code>foo2在執(zhí)行時(shí),調(diào)用foo1
,foo1
方法會(huì)從他自己的作用域開始搜索變量a1
,最終在其父級作用域中找到a1
,即a1 = 'global'
。由此可以看出,foo2
內(nèi)部的foo1
在執(zhí)行時(shí)并沒有去拿foo2
作用域中的變量a1
。
以說作用域的嵌套關(guān)系并不是在執(zhí)行時(shí)確定的,而是在定義時(shí)就確定好了的!
最后提一下全局作用域。通過字面的意思就能知道,全局作用域中的變量也好,屬性也好,在任何函數(shù)中都能直接訪問。
其中有一點(diǎn)需要注意,在任何地方?jīng)]有通過var關(guān)鍵字聲明的變量都是定義在全局變量中。其實(shí),在模塊化編程中,應(yīng)該盡量避免使用全局變量,聲明變量時(shí),無論如何都應(yīng)該避免不使用var關(guān)鍵字。
閉包是函數(shù)式編程語言的一大語言特性。w3c上關(guān)于閉包的嚴(yán)格定義如下:由函數(shù)(環(huán)境)及其封閉的自由變量組成的集合體。這句話比較晦澀難懂,反正剛開始我是沒看懂。下面通過一些例子來說明。
var closure = function() {
var count = 0;
return function() {
count ++;
return count;
};
};
var counter = closure();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
最后的結(jié)果是1,2,3。
這個(gè)demo中,closure
是一個(gè)函數(shù)(其實(shí)他相當(dāng)于一個(gè)類的構(gòu)造函數(shù)),并且返回一個(gè)函數(shù)(這個(gè)被返回的函數(shù)加上其定義環(huán)境通俗上被稱為閉包)。
在返回的函數(shù)中,引用了外部的count
變量。在var counter = closure();
這句代碼之后,counter
實(shí)際上就是一個(gè)函數(shù),這樣每次在counter()
時(shí),先將count自增然后打印出來。
這里counter
的函數(shù)內(nèi)部并沒有關(guān)于count
的定義,所以在執(zhí)行時(shí)會(huì)往上層作用域搜索,而他的上層作用域是closure
函數(shù),而不是counter()
執(zhí)行時(shí)所在的上層作用域。
為什么它的上層作用域是closure
函數(shù)呢?因?yàn)椋?/p>
closure
函數(shù)的內(nèi)部作用域,所以能夠拿到closure
函數(shù)中的count
變量。從這里可以看出,閉包會(huì)造成對原作用域和其上層作用域的持續(xù)引用。在這里,count
變量持續(xù)被引用,其所占用的內(nèi)存就不會(huì)被釋放掉。
在看下面的這個(gè)例子,
var closure = function() {
var count = 0;
return function() {
count ++;
return count;
};
};
var counter1 = closure();
var counter2 = closure();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1
console.log(counter2()); // 2
console.log(counter1()); // 3
從結(jié)果可以看出,生成的閉包實(shí)例是各自獨(dú)立的,他們內(nèi)部引用的count
變量分別屬于各自不同的運(yùn)行環(huán)境。
我們可以這樣理解,在閉包生成時(shí),將原上下文環(huán)境做了一份拷貝副本,這樣不同的閉包實(shí)例就有自己獨(dú)立的運(yùn)行環(huán)境了。
閉包目前來說有兩大用處,
$('#id0').animate({
left: '+50px'
}, 1000, function() {
$('#id1').animate({
left: '+50px'
}, 1000, function() {
$('#id2').animate({
left: '+50px'
}, 1000, function() {
alert('done');
});
});
});
Javascript的對象沒有私有成員的概念。一般的編碼規(guī)范中會(huì)要求類似_privateProp
的形式來定義私有屬性。但是這是一個(gè)非正式的約定,而且_privateProp
仍然能夠被訪問到。
我們可以通過閉包來實(shí)現(xiàn)私有成員,如下,
var student = function(yourName, yourAge) {
var name, age;
name = yourName || '';
age = yourAge || 0;
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
setName: function(yourName) {
name = yourName;
},
setAge: function(yourAge) {
age = yourAge;
}
};
}
var mamamiya = student('mamamiya', 23);
mamamiya.getName();
mamamiya.getAge();
這里我封裝了一個(gè)student
類,并設(shè)置了兩個(gè)屬性name
,age
。這兩個(gè)屬性除了通過student
對象的訪問器方法訪問之外,絕無其他的方法能夠訪問到。這里就實(shí)現(xiàn)了對部分屬性的隱藏。
Javascript的對象是基于原型的,和其他的一些面向?qū)ο笳Z言有一些區(qū)別。
我們可以通過如下的這種形式來創(chuàng)建一個(gè)js對象。
var foo = {
'a': 'baz',
'b': 'foz',
'c': function() {
return 'hello js';
}
};
我們還可以通過構(gòu)造函數(shù)來創(chuàng)建對象。
function user(name, uri) {
this.name = name;
this.uri = uri;
this.show = function() {
console.log(this.name);
}
};
var mamamiya = new user('mamamiya', 'http://blog.gejiawen.com');
mamamiya.show();
Javascript中上下文對象就是this
,他表示被調(diào)用函數(shù)所處的環(huán)境。他的作用就是在一個(gè)函數(shù)內(nèi)部引用調(diào)用它自己。
在Javascript中,任何函數(shù)都是被某個(gè)對象調(diào)用。
apply
和call
在Javascript中apply
和call
是兩個(gè)神奇的方法,他們的作用是以不同的上下文環(huán)境來調(diào)用函數(shù)。通俗點(diǎn)就是說,一個(gè)對象可以調(diào)用另一個(gè)對象的方法。
看下面的例子,
var user = {
name: 'mamamiya',
show: function(words) {
console.log(this.name + ' says ' + words);
}
};
var foo = {
name: 'baz'
};
user.show.call(foo, 'hello'); // baz says hello
這段代碼的結(jié)果是baz says hello
。這里通過call
方法改變了user.show
方法的上下文環(huán)境,user.show
方法在執(zhí)行時(shí),內(nèi)部的this
指向的是foo
對象。
bind
方法可以使用bind
方法永久的改變函數(shù)的上下文。bind
將會(huì)返回一個(gè)函數(shù)引用。
看下面的這個(gè)例子,
var user = {
name: 'mamamiya',
func: function() {
console.log(this.name);
}
};
var foo = {
name: 'baz'
};
foo.func = user.func;
foo.func(); //baz
foo.func1 = user.func.bind(user);
foo.func1(); //mamamiya
func = user.func.bind(foo);
func(); //baz
func2 = func;
func2(); //baz
其實(shí),bind
還可以在綁定上下文時(shí)附帶一些參數(shù)。
不過有時(shí)候,bind
會(huì)有一些讓人迷惑的地方,看下面這個(gè)例子,
var user = {
name: 'mamamiya',
func: function() {
console.log(this.name);
}
};
var foo = {
name: 'baz'
};
func = user.func.bind(foo);
func(); //baz
func2 = func.bind(user);
func2(); //baz
這里為什么func2
函數(shù)的輸出結(jié)果仍然是baz
呢?
也就是說,我企圖將func
的上下文環(huán)境還原到user
上為什么沒有起作用?
我們這樣來看,
func = user.func.bind(foo) ≈ function() {
return user.func.call(foo);
};
func2 = func.bind(user) = function() {
return func.call(user);
};
ok,現(xiàn)在可以看出來,func2
中實(shí)際上是以user
為this
指針調(diào)用了func
,但是在func
中并沒有使用this
。
通過構(gòu)造函數(shù)和原型都能生成對象,但是兩者之間有一些區(qū)別??聪旅娴倪@個(gè)列子,
function Class() {
var a = 'hello';
this.prop1 = 'git';
this.func1 = function() {
a = '';
};
}
Class.prototype.prop2 = 'Mercurial';
Class.prototype.func2 = function() {
console.log(this.prop2);
};
var class1 = new Class();
var class2 = new Class();
console.log(class1.func1 === class2.func1); //false
console.log(class1.func2 === class2.func2); //true
所以說,掛在prototype
上的屬性,會(huì)被不同的實(shí)例會(huì)共享。通過構(gòu)造函數(shù)創(chuàng)建出來的屬性,每一個(gè)實(shí)例都有一份獨(dú)立的副本。
function foo() { }
Object.prototype.name = 'My Object';
foo.prototype.name = 'baz';
var obj = new Object();
var foo = new foo();
console.log(obj.name); // My Object
console.log(foo.name); // baz
console.log(foo.__proto__.name); // baz
console.log(foo.__proto__.__proto__.name); // My Object
console.log(foo.__proto__.constructor.prototype.name); // baz
在Javascript中,繼承是依靠一套叫做原型鏈的機(jī)制實(shí)現(xiàn)的。
說的通俗一點(diǎn)就是,在繼承的時(shí)候,將父類的實(shí)例對象直接賦值給子類的prototype
對象,這樣子類就擁有了父類的全部屬性。子類還可以在自己的prototype對象上增加自己的特殊屬性。
看下面的例子,
function ClassA() { }
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
alert(this.color);
};
function ClassB() { }
ClassB.prototype = new ClassA();
Javascript中所有的對象類型的變量都是指向?qū)ο蟮囊?。所以在賦值和傳遞的實(shí)際上都是對象的引用。
在Javascript中,對象的復(fù)制分為淺拷貝和深拷貝。
下面的示例是淺拷貝,
Object.prototype.makeCopy = funciton() {
var newObj = {};
for (var i in this) {
newObj[i] = this[i];
}
return newObj;
};
var obj = {
name: 'mamamiya',
likes: ['js']
};
var newObj = obj.makeCopy();
obj.likes.push('python');
console.log(obj.likes); // ['js', 'python']
console.log(newObj.likes); // ['js', 'python']
從上面的代碼可以看出,淺拷貝只是復(fù)制了一些基本屬性,但是對象類型的屬性是被共享的。obj.likes
和newObj.likes
都指向同一個(gè)數(shù)組。
想要做深拷貝,并不是一件容易的事情,因?yàn)槌嘶緮?shù)據(jù)類型,還有多種不同的對象,對象內(nèi)部還有復(fù)雜的結(jié)構(gòu),因此需要用遞歸的方式來實(shí)現(xiàn)。
看下面的例子,
Object.prototype.makeDeepCopy = function() {
var newObj = {};
for (var i in this) {
if (typeof(this[i]) === 'object' || typeof(this[i]) === 'function') {
newObj[i] = this[i].makeDeepCopy();
} else {
newObj[i] = this[i];
}
}
return newObj;
};
Array.prototype.makeDeepCopy = function() {
var newArray = [];
for (var i = 0; i < this.length; i++) {
if (typeof(this[i]) === 'object' || typeof(this[i]) === 'function') {
newArray[i] = this[i].makeDeepCopy();
} else {
newArray[i] = this[i];
}
}
return newArray;
};
Function.prototype.makeDeepCopy = function() {
var self = this;
var newFunc = function() {
return self.apply(this, arguments);
}
for (var i in this) {
newFunc[i] = this[i];
}
return newFunc;
};
var obj = {
name: 'mamamiya',
likes: ['js'],
show: function() {
console.log(this.name);
}
};
var newObj = obj.makeDeepCopy();
newObj.likes.push('python');
console.log(obj.likes); // ['js']
console.log(newObj.likes); // ['js', 'python']
console.log(newObj.show == obj.show); // false
上面的示例代碼中很好的實(shí)現(xiàn)了對象,函數(shù),數(shù)組在做深拷貝的邏輯。在一般情況下都是比較好用的。但是有一種情況下,這種方法卻無能為力。如下:
var obj1 = {
ref: null
};
var obj2 = {
ref: obj1
};
obj1.ref = obj2;
上面這段代碼塊的邏輯很簡單,就是兩個(gè)相互引用的對象。
當(dāng)我們試圖使用深拷貝來復(fù)制obj1
和obj2
中的任何一個(gè)時(shí),問題就出現(xiàn)了。因?yàn)樯羁截惖淖龇ㄊ怯龅綄ο缶瓦M(jìn)行遞歸復(fù)制,那么結(jié)果只能無限循環(huán)下去。
對于這種情況,簡單的遞歸已經(jīng)無法解決,必須設(shè)計(jì)一套圖論算法,分析對象之間的依賴關(guān)系,建立一個(gè)拓?fù)浣Y(jié)構(gòu)圖,然后分別依次復(fù)制每個(gè)頂點(diǎn),并重新構(gòu)建它們之間的依賴關(guān)系。這已經(jīng)超出了這里的討論范圍,而且在實(shí)際的工程操作中 幾乎不會(huì)遇到這種需求,所以我們就不繼續(xù)討論了。
更多建議: