title: 面向對象三、作用域
date: 2017-06-17 10:10:13
tags: javascript筆記
instanceof 運算符
語法
object instanceof fn
如果運算符後面的函數的prototype屬性引用的對象出現在運算符面前對象的原型鏈上的話就返回true,否則返回false。
function foo (){
}
var f = new foo;
console.log(f instanceof foo); // 返回true 判斷f是不是foo函數的實例
console.log(f instanceof Object); // 返回true f也是在Object的原型鏈上
function fn (){
}
foo.prototype = new fn;
var ff = new foo;
console.log(ff instanceof fn) // true 因爲fn創建的對象就是foo.prototype,所以foo.prototype的原型就是fn.prototype。
console.log(ff instanceof Object) // true
//ff -> foo.prototype -> fn.prototype -> Object.prototype -> null
作用域鏈
繪製作用域鏈的規則
-
將這個script標籤的全局作用域定義爲0級作用域鏈,將全局作用域上的所有數據(變量、對象、函數),繪製在這條鏈上
-
由於在詞法作用域中,只有函數可以分割作用域,那麼只要遇到函數就再引申出新的作用域鏈,級別爲當前鏈級別+1,將數據繪製到新鏈上
-
重複步驟二,直到沒有遇到函數爲止
以下面的函數舉例來繪製作用域鏈:
var n = 123;
function f(){
var n = 12;
function f1(){
var n = 1;
function f2(){
var n = 0;
}
function f3(){
var n = 0;
}
}
}
變量的搜索原則
-
當訪問一個變量時,首先在當前變量所處的作用域上查找,如果找到就直接使用,並停止查找
-
如果沒有找到就向上一級鏈(T-1)上去查找,如果找到就直接使用並停止查找
-
如果沒有找到就繼續向上一級鏈查找,直到0級鏈
-
如果沒有找到就報錯
-
如果訪問的變量不存在,會搜索整個作用域鏈(不僅性能低,而且拋出異常)
-
在實際開發不推崇所有數據都寫在全局上。儘量使用局部變量,推薦使用沙箱。
-
如果在開發中,所有js變量都寫在全局上,會造成全局污染
-
-
同級別的鏈上的變量互不干擾
function f (a){
var a ;
function a (){
console.log(a);
}
a();
a = 10;
console.log(a);
}
f(100);
// 在這個題中 var a 不會覆蓋a的參數100,但是function會改變,a=10這個賦值操作也會覆蓋,因爲都相當於賦值。
補充
在函數執行時候,會創建一個執行的環境,這個環境包括:activeObject(活動對象)以及作用域鏈
activeObject存儲的是所有在函數內部定義的變量,以及函數的形參;
會將變量名字以及形參名字作爲該對象的屬性來存儲,比如有個變量a,那麼就等於有了a這個屬性,這時a的屬性值就是100;
因爲之前已經傳了a這個參數,傳了參數也相當於在函數內聲明瞭a這個變量,也就是說此時在activeObject中已經有了a這個屬性,所以這時在函數內聲明a就不管用了,只有賦值才管用。只能改屬性值但屬性不會再創建。上述代碼先將函數賦值給了a,又將100賦值給了a
查找對象也是在activeObject中查找,也就是查找裏邊的屬性和屬性值,沒有的話就找上一級函數的activeObject。直到找到爲止,沒有找到就報錯。
閉包
定義
-
指一個函數有權去訪問另一個函數內部的參數和變量。
-
創建閉包的最常見的方式就是在一個函數內創建另一個函數,通過另一個函數訪問這個函數的局部變量。
-
應用閉包主要是爲了設計私有的方法和變量。
-
一般函數執行完畢後,局部活動對象就被銷燬,內存中僅僅保存全局作用域,但是閉包的情況不同,不會被垃圾回收機制回收。
-
爲了防止閉包導致的內存泄漏,用完閉包之後手工賦值爲null,就會被回收。
-
閉包結構和閉包引用寫在同一個函數裏,出了函數就自動刪除該緩存了。
缺點
-
閉包會造成函數內部的數據常駐內存,會增大內存使用量,從而引發內存泄漏問題。每創建一個閉包都會創建一個緩存數據,這樣就會造成內存泄漏(內存滿了後其他數據寫不進去)
-
閉包會使變量始終保存在內存中,如果不當使用會增大內存消耗。
function fn (){
var n = Math.random();
function getN (){
return n; // 這個作用域中沒有n所以會向上尋找。
}
return getN; //這裏是要返回整個getN函數,所以不加括號。
}
var ff = fn(); // 這個ff就是閉包,通過它可以訪問fn內部的數據。
var nn = ff();
var mm = ff(); // fn()實際上是getN這個函數體,那麼ff()就是調用了getN這個函數,這樣會返回n。
console.log(nn);
console.log(mm); // nn和mm的數是相同的
console.log(nn === mm); //true,
ff = null; // n被回收
優點
-
希望一個變量長期駐紮在內存中
-
避免全局變量的污染
-
私有成員的存在
閉包的應用
下面通過幾個案例來了解閉包的優點:
統計某個構造函數創建多少個對象,變量可以長駐內存
//統計某個構造函數創建多少個對象
function counter() {
var n = 0;
return {
add:function(){
n+=1;
},
getCounts:function(){
return n;
}
}
}
// 創建一個閉包,相當於初始化計時器,因爲重新調用會讓n=0.
// 然後創建閉包時,n=0和return的對象會被緩存。
// 那麼爲什麼閉包環境能緩存數據呢:
// 因爲 var n = 0相當於n進入環境,在局部作用域創建了一個對象和n 最後把對象和n返回給外部作用域,相當於已出執行環境,通過全局變量就能找到返回的對象,通過返回的對象就能找到n,通過這個路徑就能找到變量n,
// 所以得出結論因爲在函數內部有方法(函數)對其有引用,並且又將其返回到外部作用域上的一個變量接收。創建之後就緩存了,這時再通過這個變量訪問閉包裏的環境,那麼只會訪問該變量的緩存區域。
var PresonCount = counter();
function Preson(){
PresonCount.add();
}
//用Preson這個構造函數創建對象,每創建一次都相當於調用了一次該構造函數。
var p = new Preson()
var p1 = new Preson()
var p2 = new Preson()
var p3 = new Preson()
console.log(PresonCount.getCounts()); // 打印4
局部變量的累加,怎樣做到變量a即是局部變量又可以累加
// 1、全局變量
var a = 1
function abc(){
a++
console(a)
}
abc() // 2
abc() // 3
// 可以累加但問題是a是全局變量 容易被污染
// 2、局部變量
function abc () {
var a = 1;
a++;
console(a);
}
abc() // 2
abc() // 2
// 放到局部裏又不能累加,因爲每次執行函數都相當於把a重新聲明
// 3、局部變量的累加
function outer () {
var a = 1;
return function () {
a++;
console.log(a);
}
}
var y = outer();
y() // 2
y() // 3
// 這樣即實現了累加,又能把變量a藏起來。
模塊化代碼,減少全局變量的污染。a是局部變量,全局變量有a也沒關係
var abc = (function () {
var a = 1;
return function(){
a++
console(a)
}
}()); // 函數在這裏自調用一次,所以abc得到的是abc裏返回的函數
abc(); // 2
abc(); // 3
函數的私有成員調用
var aaa = (function(){
var a = 1;
function bbb(){
a++;
console.log(a);
}
function ccc(){
a++;
console.log(a);
}
return {
b:bbb,
c:ccc // json格式,也就是返回一個對象。b是bbb的函數體
}
}()); // 自調用一下,這樣aaa就是函數體內的返回值,也就是那個json格式的對象
aaa.b(); //2
aaa.c(); //3
在循環中直接找到對應元素的索引
//這是以前的寫法
var lis = document.getElementsByTagName('li');
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function(){
console.log(i); // 由於進入函數時i已經循環完畢,所以i變爲常量4
}
// 用閉包的方式來寫
for (var i = 0; i < lis.length; i++) {
(function(i){
lis[i].onclick = function(){
console.log(i);
}
}(i)) //在這裏調用一次,將i作爲參數傳進去,這時裏邊的i就不會是執行完之後的i值
}
內存泄漏問題
由於IE的js對象和DOM對象使用不同的垃圾收集方法,因此閉包在IE中會導致內存泄露問題,也就是無法銷燬駐留在內存中的元素
function closure(){
var oDiv = document.getElementById('oDiv'); //用完之後會一直待在內存中
var test = oDiv.innerHTML;
oDiv.onclick = function () {
alert(test); // 這裏用oDiv導致內存泄漏
};
oDiv = null; //最後應該將oDiv解除來避免內存泄漏
}
多閉包結構
像上邊的案例只需要一個n的值一個閉包就可以解決,而很多時候需要返回的變量大於1。
如下需要訪問函數內部的多個變量n和m,就需要多個閉包。閉包的實質就是一個函數。
function foo(){
var n = 1,m = {age:20}; // n是變量,m是對象
function getN(){
return n;
}
function getM(){
return m;
}
return {getM:getM,getN:getN}; // :前的是屬性名,:後的是屬性值也就是函數體。
}
var obj = foo(); // 這就是一次閉包
obj.getM().age = 22;
console.log(obj.getM().age); // 22
console.log(obj.getN()); // 1
var obj1 = foo(); // 這是第二次閉包,每閉包一次就是重新調用一次。不會被上次obj閉包調用並且更改屬性值而改變函數本身的值,這和原型的不可變特性比較像。
console.log(obj1.getM().age); // 20
對象的私有屬性
// 用下面這個案例來說明構造函數的問題。
function Preson (name,age) {
this.name = name;
this.age = age;
}
// 這是創建對象並且傳參姓名
var xiaohong = new Preson("小紅",20)
// 這時如果一不小心,就能隨意將姓名改成小綠了。
xiaohong.name = "小綠"
// ---------------------------------------------------------------------------------------
// 爲了解決這個問題,可以用這種寫法
function Preson (name,age) {
return {
getName:function(){
return name;
},
getAge:function(){
return age;
}
// name通常不能更改,但是age 可以改,給了這樣一個接口就可以直接改了
setAge:function(val){
age = val;
}
}
}
// 還是創建對象並且傳參
// 這樣就沒法隨意更改了,除非更改構造函數的函數。
var xiaohong = new Preson("小紅",20)
xiaohong.serAge(19);
xiaohong.getAge(); // 先傳一個參數19,讓age改爲19.再調用一下getAge函數。就將年齡屬性改爲了19
// 但是還有個問題,那就是通過下面的語句可以創建一個name的屬性。這樣也是不太好的
xiaohong.name = "小綠"
//通過下面這個屬性可以解決。但是要寫在上面創建屬性的語句的前面
Object.preventExtenions(xiaohong)
xiaohong.name = "小綠"
console.log(xiaohong.name) // 這時就返回undefined了。
用閉包來解決遞歸函數性能問題
// 利用閉包可以緩存數據的特性,改善遞歸性能
// 這個函數是爲了緩存
var fib = (function() {
var cache = [];
// 這個函數是求fib的第n項值
return function(n) {
if (n < 1) {
return undefined;
}
// 1、看緩存裏有沒有
// 如果有,直接返回值
if (cache[n]) {
return cache[n]
} else
// 如果沒有重新計算
if (n === 1 || n === 2) {
cache[n] = 1;
} else {
cache[n] = arguments.callee(n - 1) + arguments.callee(n - 2);
}
return cache[n];
}
}())
console.log(fib(10));
垃圾回收機制
定義
GC(Garbage Collection),專門負責一些無效的變量所佔有的內存回收銷燬。
原理
垃圾收集器會定期(週期性)找出那些不在繼續使用的變量,然後釋放其內存。但這個過程不是實時的,因爲其開銷比較大,所以垃圾回收器會照固定的時間間隔週期性的執行。
爲什麼閉包會造成內存常駐,並且讓垃圾回收機制不能回收
不再使用的變量(生命週期結束的變量),當然只可能是局部變量,全局變量的生命週期直至瀏覽器卸載頁面纔會結束。局部變量只在函數的執行過程中存在,而在這個過程中會爲局部變量在棧或堆上分配相應的空間,以存儲它們的值,然後在函數中使用這些變量,直至函數結束,而閉包中由於內部函數的原因,外部函數並不能算是結束。
function fn1 () {
// body...
var obj = {
name:'tom',
age:20
}
}
function fn2 () {
// body...
var obj = {
name:'tom',
age:20
}
return obj
}
var a = fn1();
var b = fn2();
當fn1被調用時,進入fn1環境,會開闢一塊內存存放對象obj,而當調用結束後,出了fn1的環境,那麼該塊內存會被js引擎中的垃圾回收器自動釋放,
而在fn2被調用的過程中,返回的對象被全局變量b所指向,所以該塊內存並不會被釋放。那麼問題出現了:到底哪個變量是沒有用的?所以垃圾收集器必須跟蹤到底哪個變量沒用,對於不再有用的變量打上標記,以備將來收回其內存。用於標記的無用變量的策略可能因實現而有所區別,通常情況下有兩種實現方式:計數清除和引用清除。
引用計數法
跟蹤記錄每個值被引用的次數,如果一個變量被另外一個變量引用了, 那麼該變量的引用計數+1,如果同一個值又被賦值給另一個變量,則引用次數再+1。相反,當這個變量不再引用該變量時,這個變量的引用計數-1;GC會在一定時間間隔去查看每個變量的計數,如果爲0就說明沒有辦法再訪問這個值了就將其佔用的內存回收。
function test () {
var a = {}; // a的引用次數爲0
var b = a ; // a的引用次數+1,爲1
var c = a ; // a的引用次數再+1, 爲2
var b = {} // a的引用次數減1,爲1
}
引用計數的缺點
function test () {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
以上代碼a和b的引用次數都是2,fn()執行完畢後,兩個對象已經離開環境,在標記清除方式下是沒問題,但在引用計數策略下,因爲a和b的引用次數不爲0,所以不會被垃圾回收器回收內存,如果fn函數被大量調用,就會造成內存泄漏。只能手動讓a和b=null才能被識別並回收
window.οnlοad=function outerFunction(){
var obj = document.getElementById("element");
obj.οnclick=function innerFunction(){};
};
這段代碼看起來沒什麼問題,但是obj引用了document.getElementById("element")而document.getElementById("element")的onclick方法會引用外部環境值中的變量,自然也包括obj。
解決辦法:自己手工解除循環引用。
window.οnlοad=function outerFunction(){
var obj = document.getElementById("element");
obj.οnclick=function innerFunction(){};
obj = null;
};
將變量設置爲null意味着切斷變量與它此前引用的值之間的連接。當垃圾回收器下次運行時,就會刪除這些值並回收它們佔用的內存。
標記清除法
從當前文檔的根部(window對象)找一條路徑,如果能到達該變量,那麼說明此變量有被其他變量引用,也就說明該變量不應該被回收掉,反之,應該被回收其所佔的內存
當變量進入某個執行環境(例如,在函數中聲明一個變量),那麼給其標記爲“進入環境”,此時不需要回收,但是如果上述執行環境執行完畢,便被銷燬,那麼該環境內的所有變量都被標記爲“已出環境”,如果被標記爲已出環境,就會被回收掉其佔用的內存空間。
function test() {
var a = 10; // 被標記,進入環境
var b = 20; // 被標記,進入環境
}
test() // 執行完畢後,a,b被標記離開環境,被回收。
垃圾回收器在運行時會給存儲在內存中的所有變量都加上標記,然後,它會去掉環境中的變量以及環境中的變量引用的變量的標記(閉包)。而在此之後再被加上標記的變量將被視爲準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最後,垃圾回收器完成內存清除工作,銷燬那些帶標記的值並回收它們所佔用的內存空間。目前IE,Firefox,Opera,Chrome,Safari的js實現使用的都是標記清除的垃圾回收策略,只不過時間間隔不相同。
沙箱
變量不寫在全局上,但又想達到寫在全局的目的,就用沙箱
特點:
-
能分割作用域,不會污染全局(函數)
-
在分割後的作用域的內部的代碼要自執行。(匿名函數)
// 結構:
(function(){
//代碼塊
}());
// 經典的沙箱模式:
var n = 2
(function () {
// 這個n不會污染外部的n。所以這樣能保證自己的代碼安全執行(別人也污染不了我),也不會污染全局變量或其他作用域的變量
var n = 1;
function foo () {
console.log(n);
}
//window.fn 相當於設定了一個全局變量
window.fn = foo;
}())
fn();