感謝Node.js開發(fā)指南,參考了它的附錄部分內容。
Javascript中的作用域是通過函數來確定的,這一點與C
、Java
等靜態(tài)語言有一些不一樣的地方。
if (true) {
var a = 'Value';
}
console.log(a); // Value
上面的代碼片段將會輸出Value。(在瀏覽器環(huán)境中)
再來一個更加common的例子,
var a1 = 'Valve';
var foo1 = function() {
console.log(a1);
};
foo1(); // Value
var foo2 = function() {
var a1 = 'DOTA2';
console.log(a1);
}
foo2(); // DOTA2
顯然,foo1
的結果是Value,foo2
的結果是DOTA2,這應該很容易理解。
接下來這個例子將會讓人感到迷惑,
var a1 = 'mercurial';
var foo = function() {
console.log(a1);
var a1 = 'git';
}
foo(); // undefined
此時,結果將會是undefined
。
因為在函數foo內部的a1將會覆蓋函數外部的變量a1,js搜索作用域是按照從內到外的,而且當執(zhí)行到console.log時,函數作用域內部的a1還尚未被初始化,所以會輸出undefined。
其實這里還涉及到一個變量懸置的概念,即在Javascript的函數中,無論在何處聲明或者初始化的變量都等效于函數的起始位置聲明,在實際位置賦值。如下,
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
}
最后還有一點需要說明的就是,未定義變量和定義但未被初始化的變量,雖然他們的值輸出都是undefined,但是在js內部的實現上還是有區(qū)別的。未定義的變量存在于js的局部語義表上,但是未被分配內存,而定義卻未初始化的變量時實際分配了內存的。
接下來這個例子將會演示函數作用域的嵌套,
var foo = function() {
var a1 = 'foo';
(function() {
var a1 = 'foo1';
(function() {
console.log(a1);
})();
})();
};
foo(); // foo1
輸出結果是foo1。這里我在最內層的console.log
中打印a1,此時,因為最內層的作用域中沒有a1的相關定義,所以會往上層作用域搜索,得到a1=’foo1’。這里實際上有一個嵌套的作用域關系。
這里還有一點需要注意,就是函數作用的嵌套關系是在定義時就會確定的,而非調用的時候。也即js的作用域是靜態(tài)作用域,好像又叫詞法作用域,因為在代碼做語法分析時就確定下來了??聪旅娴倪@個例子,
var a1 = 'global';
var foo1 = function() {
console.log(a1);
};
foo1(); // global
var foo2 = function() {
var a1 = 'locale';
foo1();
};
foo2(); // global
示例的輸出結果都將會是global
。foo1()
的執(zhí)行結果為global
不需要太多的解釋,很容易明白。
因為foo2
在執(zhí)行時,調用foo1
,foo1
方法會從他自己的作用域開始搜索變量a1
,最終在其父級作用域中找到a1
,即a1 = 'global'
。由此可以看出,foo2
內部的foo1
在執(zhí)行時并沒有去拿foo2
作用域中的變量a1
。
以說作用域的嵌套關系并不是在執(zhí)行時確定的,而是在定義時就確定好了的!
最后提一下全局作用域。通過字面的意思就能知道,全局作用域中的變量也好,屬性也好,在任何函數中都能直接訪問。
其中有一點需要注意,在任何地方沒有通過var關鍵字聲明的變量都是定義在全局變量中。其實,在模塊化編程中,應該盡量避免使用全局變量,聲明變量時,無論如何都應該避免不使用var關鍵字。
閉包是函數式編程語言的一大語言特性。w3c上關于閉包的嚴格定義如下:由函數(環(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
最后的結果是1,2,3。
這個demo中,closure
是一個函數(其實他相當于一個類的構造函數),并且返回一個函數(這個被返回的函數加上其定義環(huán)境通俗上被稱為閉包)。
在返回的函數中,引用了外部的count
變量。在var counter = closure();
這句代碼之后,counter
實際上就是一個函數,這樣每次在counter()
時,先將count自增然后打印出來。
這里counter
的函數內部并沒有關于count
的定義,所以在執(zhí)行時會往上層作用域搜索,而他的上層作用域是closure
函數,而不是counter()
執(zhí)行時所在的上層作用域。
為什么它的上層作用域是closure
函數呢?因為,
closure
函數的內部作用域,所以能夠拿到closure
函數中的count
變量。從這里可以看出,閉包會造成對原作用域和其上層作用域的持續(xù)引用。在這里,count
變量持續(xù)被引用,其所占用的內存就不會被釋放掉。
在看下面的這個例子,
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
從結果可以看出,生成的閉包實例是各自獨立的,他們內部引用的count
變量分別屬于各自不同的運行環(huán)境。
我們可以這樣理解,在閉包生成時,將原上下文環(huá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ī)范中會要求類似_privateProp
的形式來定義私有屬性。但是這是一個非正式的約定,而且_privateProp
仍然能夠被訪問到。
我們可以通過閉包來實現私有成員,如下,
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();
這里我封裝了一個student
類,并設置了兩個屬性name
,age
。這兩個屬性除了通過student
對象的訪問器方法訪問之外,絕無其他的方法能夠訪問到。這里就實現了對部分屬性的隱藏。
Javascript的對象是基于原型的,和其他的一些面向對象語言有一些區(qū)別。
我們可以通過如下的這種形式來創(chuàng)建一個js對象。
var foo = {
'a': 'baz',
'b': 'foz',
'c': function() {
return 'hello js';
}
};
我們還可以通過構造函數來創(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
,他表示被調用函數所處的環(huán)境。他的作用就是在一個函數內部引用調用它自己。
在Javascript中,任何函數都是被某個對象調用。
apply
和call
在Javascript中apply
和call
是兩個神奇的方法,他們的作用是以不同的上下文環(huán)境來調用函數。通俗點就是說,一個對象可以調用另一個對象的方法。
看下面的例子,
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
這段代碼的結果是baz says hello
。這里通過call
方法改變了user.show
方法的上下文環(huán)境,user.show
方法在執(zhí)行時,內部的this
指向的是foo
對象。
bind
方法可以使用bind
方法永久的改變函數的上下文。bind
將會返回一個函數引用。
看下面的這個例子,
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
其實,bind
還可以在綁定上下文時附帶一些參數。
不過有時候,bind
會有一些讓人迷惑的地方,看下面這個例子,
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
函數的輸出結果仍然是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,現在可以看出來,func2
中實際上是以user
為this
指針調用了func
,但是在func
中并沒有使用this
。
通過構造函數和原型都能生成對象,但是兩者之間有一些區(qū)別??聪旅娴倪@個列子,
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
上的屬性,會被不同的實例會共享。通過構造函數創(chuàng)建出來的屬性,每一個實例都有一份獨立的副本。
那么,什么叫原型鏈?
JavaScript中有兩個特殊的對象:Object
與Function
,它們都是構造函數,用于生成對象。Object.prototype
是所有對象的祖先,Function.prototype
是所有函數的原型,包括構造函數。
我把JavaScript中的對象分為三類,
new
語句顯式構造的對象new
調用生成普通對象的函數prototype
屬性指向的對象這三類對象中每一種都有一個__proto__
屬性,它指向該對象的原型。任何對象沿著它開始遍歷都可以追溯到Object.prototype
。
構造函數對象有prototype
屬性,指向一個原型對象,通過該構造函數創(chuàng)建對象時,被創(chuàng)建對象的__proto__
屬性將會指向構造函數的prototype
屬性。原型對象有constructor
屬性,指向它對應的構造函數。
看下面的這個例子,幫助理解,
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中,繼承是依靠一套叫做原型鏈的機制實現的。
說的通俗一點就是,在繼承的時候,將父類的實例對象直接賦值給子類的prototype
對象,這樣子類就擁有了父類的全部屬性。子類還可以在自己的prototype對象上增加自己的特殊屬性。
看下面的例子,
function ClassA() { }
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
alert(this.color);
};
function ClassB() { }
ClassB.prototype = new ClassA();
Javascript中所有的對象類型的變量都是指向對象的引用。所以在賦值和傳遞的實際上都是對象的引用。
在Javascript中,對象的復制分為淺拷貝和深拷貝。
下面的示例是淺拷貝,
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']
從上面的代碼可以看出,淺拷貝只是復制了一些基本屬性,但是對象類型的屬性是被共享的。obj.likes
和newObj.likes
都指向同一個數組。
想要做深拷貝,并不是一件容易的事情,因為除了基本數據類型,還有多種不同的對象,對象內部還有復雜的結構,因此需要用遞歸的方式來實現。
看下面的例子,
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
上面的示例代碼中很好的實現了對象,函數,數組在做深拷貝的邏輯。在一般情況下都是比較好用的。但是有一種情況下,這種方法卻無能為力。如下:
var obj1 = {
ref: null
};
var obj2 = {
ref: obj1
};
obj1.ref = obj2;
上面這段代碼塊的邏輯很簡單,就是兩個相互引用的對象。
當我們試圖使用深拷貝來復制obj1
和obj2
中的任何一個時,問題就出現了。因為深拷貝的做法是遇到對象就進行遞歸復制,那么結果只能無限循環(huán)下去。
對于這種情況,簡單的遞歸已經無法解決,必須設計一套圖論算法,分析對象之間的依賴關系,建立一個拓撲結構圖,然后分別依次復制每個頂點,并重新構建它們之間的依賴關系。這已經超出了這里的討論范圍,而且在實際的工程操作中 幾乎不會遇到這種需求,所以我們就不繼續(xù)討論了。
更多建議: