深入學習JavaScript(二)


函數表達式和函數聲明

函數聲明

 function 函數名(參數){函數體}

函數表達式

function 函數名(可選)(參數){函數體}

###示例:
function foo(){} // 聲明,因為它是程序的一部分
var bar = function foo(){}; // 表達式,因為它是賦值表達式的一部分
new function bar(){}; // 表達式,因為它是new表達式

(function(){
function bar(){} // 聲明,因為它是函數體的一部分
  })();

另外一種不太常見的函數表達式

function foo(){}//函數聲明
(function foo(){});//函數表達式,因為在外面加上了一個分組操作符,分組操作符有一個作用是將括號中的函數聲明轉化成為函數表達式
//驗證分組操作符內是否一定要是函數聲明
try{
	(var x=5);  //var x=5是一個語句,不是函數聲明
}catch(err){
	alert(err);
}

函數表達式與函數聲明各自的作用?

函數聲明會在函數表達式被解析和求和之前先被解析,如果是函數表達式在函數聲明之前那么函數聲明也會在其之前解析

示例:

alert(foo());
function foo(){
	return "Hello World";
}

提示:函數聲明最好不要在判斷語句中使用,因為在判斷語句中函數聲明是沒有被標准化的,在不同的瀏覽器環境中可能會有不同的效果

函數聲明只能出現在程序或函數體內。從句法上講,它們 不能出現在Block(塊)({ ... })中,例如不能出現在 if、while 或 for 語句中。因為 Block(塊) 中只能包含Statement語句, 而不能包含函數聲明這樣的源元素。另一方面,仔細看一看規則也會發現,唯一可能讓表達式出現在Block(塊)中情形,就是讓它作為表達式語句的一部分。但是,規范明確規定了表達式語句不能以關鍵字function開頭。而這實際上就是說,函數表達式同樣也不能出現在Statement語句或Block(塊)中(因為Block(塊)就是由Statement語句構成的)。


函數表達式的名字只在新定義的函數作用域內有效,因為規范規定了標示符不能在外圍的作用域內有效:

示例:

var t=text(){
	return typeof text; //text在內部作用域有效
};
typeof text;  //undefined
t();  //undefined

如果是只對方法的內部有限的話,話句話說也就是外部沒法引用,那么我們為什么不用匿名函數呢?原因是因為顯示函數有利於我們的調試,因為調試的過程中我們可以很清楚的知道調試棧中的對象是那個,相比於匿名函數比較方便

另外,還有一個函數表達式跟函數聲明之間的區別是:函數聲明是不能進行自執行的,但是函數表達式卻是可以的

webkit的displayName屬性

WebKit引入了一個“特殊的”displayName屬性(本質上是一個字符串),如果開發人員為函數的這個屬性賦值,則該屬性的值將在調試器或性能分析器中被顯示在函數“名稱”的位置上

JavaScript的Module模式

基本用法

//相當於是一個類
var module=function(eq){
	//定義變量
	var VALUE="6",
		my={};
	//定義屬性
	my.Name="xiecanyong";
	my.money=function(data){
		text(data);//調用方法
	}
	//定義函數
	function text(data){
		console.log(data);
	}
	//暴露公開成員
	/*return {
		add:function(x,y){
			var count=x+y;
		}
	}*/
	return my;//暴露公開的成員活或者是屬性方法
}
var MO=new module(); //創建對象
var NAME=MO.Name;  //獲取屬性
var MONEY=MO.money(1);//給屬性值傳參

另外由於我們可以在方法中聲明一個全局變量,為了保證內部環境不對外部環境造成污染,所以我們可以通過以下兩種方法來實現:

1、引入全局變量

2、匿名閉包

引入全局變量和匿名函數

最好的例子就是jquery的源碼了,簡單的了解一下

(function($){
	//jquery源碼
}(jquery));

Module模式高級用法

拓展

我們一般使用閉包都是將相關的代碼寫在同一個文件下面,但是如果是項目工程比較大的話,需要多人合作共同完成,那么怎樣將一個文件分離,達到多人協作呢?

var blogModule = (function (my) {
my.AddPhoto = function () {
    //添加內部代碼  
};
return my;
} (blogModule));

上面的這一段代碼的運行原理:通過創建一個blogModule全局變量,然后每次調用都會將blogModule中的方法添加到blogModule全局變量中,這樣待到全部的文件加載完成后,全局變量中也就有了加載的全部方法和變量

但是這一段代碼雖然語法上是正確的,但是運行確實會報錯,這里面的原因是:因為第一次運行的時候blogMudule是一個已定義但未初始化的變量,所以向其中添加方法會發生異常

所以我們應該這樣處理,加上一個判斷時候對象未初始化,這樣就可以解決問題了

var blogModule = (function (my) {
my.AddPhoto = function () {
    //添加內部代碼  
};
return my;
} (blogModule||{}));

如果是所有的調用都是采用這種形式的話,也就意味着不論哪個文件先加載最后的blogModule全局變量都是一樣的,這樣的過程我們稱為松耦合拓展

緊耦合拓展

既然有松耦合拓展那么對應的就有緊耦合拓展,緊耦合拓展顧名思義就是不能打亂文件的加載順序但是這樣比起松耦合拓展來說會使得使用起來更為的不方便,但是卻卻可以實現方法重載的目的

var blogModule = (function (my) {
    my.oldAddPhotoMethod = my.AddPhoto;
	//將之前調用的方法傳遞給oldAddPhotoMethod,然后這樣我們再重載AddPhoto的時候就不會將原來的函數覆蓋掉
    my.AddPhoto = function () {
        // 重載方法,依然可通過oldAddPhotoMethod調用舊的方法
    };	
    return my;
} (blogModule));

緊耦合拓展在使用的時候要注意的是,全局變量中的var一定要加上,否則會出錯,這個原因后面將會說明。通過對這個緊耦合拓展的分析我們可以知道,在調用者段代碼之前,必須要有另外一段AddPhoto的方法,這里如果是加載方法的時候與前面的方法發生了沖突,那么我們就將前面的方法賦值給另外一個變量,這樣就可以實現重載(這里的重載可能與強語言的重載有一些不一樣),還有一點也是要注意的,雖然緊耦合的傳入參數不需要判斷是否為空,但是在緊耦合拓展的文件加載上,第一個加載的文件的傳入參數也是要blogModule||{}


JavaScript實現繼承

JavaScript雖然沒有想其他的語言一樣的繼承關系,但是我們可以通過將被繼承的對象(父類)傳遞給繼承對象(子類)的原型(prototype),這樣如果子類沒有定義的方法那么就會調用父類中的方法,這原理跟其他語言的繼承的定義基本上是一樣的,所以也就可以實現繼承

function MadDog() {
this.yelp = function() {
      var self = this;          
      setInterval(function() {
            self.wow();      
      }, 500);
  }
}
MadDog.prototype = new Dog();         

//for test
var dog = new Dog();
dog.yelp();
var madDog = new MadDog();
madDog.yelp();

這里只是一個簡單的原型繼承的應用,下面就來深入講解一下繼承的問題。

JavaScript是一種面向對象的語言,所以JavaScript也就具備了面向對象的3大特性(封裝、繼承、多態),但是JavaScript的繼承與其他語言的繼承有些不一樣,其他語言的繼承是基於類模型繼承,而JavaScript是基於原型模型的繼承,繼承的形式則是通過賦值的方式來進行

其實在JavaScript中還存在着一些關鍵字的概念,只不過是比較遺憾一些,沒有特定申明。例如我們可以通過

$(function(){
var test=new cal(2,1);
test.add(1,2);
});

var cal=function(first,second){
    this.first=first; //相當於將外部的值賦值給cal對象中的first變量
    this.second=second;
};
cal.prototype=function(){}();//將cal對象的父類設置為空
cal.prototype=function(){
    add=function(x,y){
        return x+y+this.first; //this.first是相當於對cal對象方法外面的變量的引用
    },
    subtract=function(x,y){
        return x-y;
    }
    return{
    	add:add,
        subtract:subtract
    }
}();

在上面的例子中,我們可以看到父類聲明的變量在子類中是能夠被訪問到的(this.first的引用)而且我們也就定義了一個類似於其他語言(如:java、C#)中的類概念,但是如果我們想對這個類進行拓展,我們可以這樣來修改。

$(function(){
var test=new cal(2,1);
test.add(1,2);
});

var cal=function(first,second){
    this.first=first;
    this.second=second;
};

cal.prototype=function(){
    add=function(x,y){
        return x+y+this.first;
    },
    subtract=function(x,y){
        return x-y;
    }
    return{
    	add:add,
        subtract:subtract
    }
}();
cal.prototype.mul=function(x,y){
	return x*y;
};
cal.prototype.add=function(x,y){
	return x+y+this.first+this.second;
};

但是如果像上面一樣添加了一個原型中已經存在的方法,那么會對這個方法進行重寫

通過這樣一層一層的繼承,形成了一個繼承的鏈式關系,我們稱這種鏈式關系為原型鏈。通過上面的學習,我們知道子類是可以調用父類,而且這樣的一種繼承方式屬於單繼承(區別於C++的多繼承方式),但是如果是原型鏈比較長,而且在這條鏈中也包含了兩個或者以上的屬性、方法,那么就會向上查找直到查找到object。萬一還是沒有找到的話,那么就只能返回undefined,如果找到,則返回最原型鏈最底層出現的方法、屬性。

function foo() {
        this.add = function (x, y) {
            return x + y;
        }
    }

    foo.prototype.add = function (x, y) {
        return x + y + 10;
    }

    Object.prototype.subtract = function (x, y) {
        return x - y;
    }

    var f = new foo();
    alert(f.add(1, 2)); //結果是3,而不是13
    alert(f.subtract(1, 2)); //結果是-1

還有一點我們也是要注意的,原型鏈上面是不能直接進行賦值的,例如:

function Foo() {}
Foo.prototype = 1; // 無效

hasOwnProperty函數:

這個方法我們在第一節中已經提過了,這里也就不再重復,我們就直接貼代碼運行一下就行了

// 修改Object.prototype
Object.prototype.bar = 1; 
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

如果是發生了像上面一樣被重寫的事情,那么我們是不能像這樣直接使用的,在object類上進行獲取

var foo = {
hasOwnProperty: function() {
    return false;
},
bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 總是返回 false

// 使用{}對象的 hasOwnProperty,並將其上下為設置為foo
{}.hasOwnProperty.call(foo, 'bar'); // true

<font size="4">詳細的原理圖如下所示<br>

通過這個圖我們知道,我們直接調用的是f00對象下面被重寫的hasOwenProperty,所有要調用系統提供的hasOwenProperty方法就需要在Object上調用,創建一個空的對象,因為所有的對象都會繼承自Object,所以在空的對象上調用時再合適不過的

跨文件共享私有對象

通過上面的拓展的介紹,你一定是對JS的團隊開發有了一定的了解,但是如果是團隊之間要共享一些方法,那么我們應該怎樣去實現呢?這就需要我們能夠對代碼里面的變量進行共享

var blogModule = (function (my) {
var _private = my._private = my._private || {},
    //應用聲明周期內,調用開鎖
    _unseal = my._unseal = my._unseal || function () {
        my._private = _private;
        my._seal = _seal;
        my._unseal = _unseal;
    };
	//加載完成后上鎖,阻止外部訪問
	_seal = my._seal = my._seal || function () {
        delete my._private;
        delete my._seal;
        delete my._unseal;
        
    },
    
	return my;
} (blogModule || {}));

子模塊

我們在上面討論的是一個單模塊的應用,但是在實際的項目中,我們有可能會因為工程過大,所以我們需要再單模塊的基礎上去創建多個子模塊

示例:

blogModule.test=(function (){
	
}());

值得一提的是方法后面的自執行的括號是可以去掉的,這個在正常的開發中比較少見,所以在此一提

自執行

一般我們的自執行是這樣定義的

var foo=function(){/*代碼*/};

這一段代碼是可以正常運行的,但是不是所有的代碼都可以加個()就自執行

function (){/*代碼*/}()

這段代碼執行會報錯,因為解析器在解析全局function或者是function內部function關鍵字的時候,默認解析為函數聲明,但是由前面的知識我們可以知道,函數聲明是必須要命名的,這樣一來,解析器就解析成為未命名的function,這樣就出錯了

但是如果你將代碼寫為如下的這種形式,那這樣是否就能正常運行呢?

function foo(){/*代碼*/}()

答案是不能正常運行,從這一段代碼上來看,是好像一段帶自執行的函數聲明,但是函數聲明后面的括號不想函數表達式是自執行的意思,而是分組操作符,分組操作符是不允許為空的, 所以這段代碼出錯

function foo(){ /* code */ }( 1 );

上面的這一段代碼就不會發生錯誤,但是也不會執行

為了解決上面的問題,我們應該在其外面加上括號,這個的原理在上面我們已經有提過了,就是因為分組操作符會將里面的函數聲明解析成為函數表達式,而函數表達式后面的括號指的是自執行

(function foo(){/*代碼*/}())

下面我們就來通過例子了解什么是自執行和立即調用

// 這是一個自執行的函數,函數內部執行自身,遞歸
	function foo() { foo(); }

// 這是一個自執行的匿名函數,因為沒有標示名稱
// 必須使用arguments.callee屬性來執行自己
	var foo = function () { arguments.callee(); };

// 這可能也是一個自執行的匿名函數,僅僅是foo標示名稱引用它自身
// 如果你將foo改變成其它的,你將得到一個used-to-self-execute匿名函數
	var foo = function () { foo(); };

// 有些人叫這個是自執行的匿名函數(即便它不是),因為它沒有調用自身,它只是立即執行而已。
	(function () { /* code */ } ());

// 為函數表達式添加一個標示名稱,可以方便Debug
// 但一定命名了,這個函數就不再是匿名的了
	(function foo() { /* code */ } ());

// 立即調用的函數表達式(IIFE)也可以自執行,不過可能不常用罷了
	(function () { arguments.callee(); } ());
	(function foo() { foo(); } ());

// 另外,下面的代碼在黑莓5里執行會出錯,因為在一個命名的函數表達式里,他的名稱是undefined
// 呵呵,奇怪
	(function foo() { foo(); } ());

這里我們出現了arguments.callee()之類的方法,下面我們就來介紹一下這些是什么?

aruguments對象代表正在執行的函數和調用它的函數的參數,calle方法返回正在執行的對象,其他的方法參數和用法詳見

js的隱含參數(arguments,callee,caller)使用方法

js中的caller和callee屬性

最后我們就貼出一個Module模型的例子來分享一下

// 創建一個立即調用的匿名函數表達式
// return一個變量,其中這個變量里包含你要暴露的東西
// 返回的這個變量將賦值給counter,而不是外面聲明的function自身

var counter = (function () {
var i = 0;

return {
    get: function () {
        return i;
    },
    set: function (val) {
        i = val;
    },
    increment: function () {
        return ++i;
    }
};
} ());

// counter是一個帶有多個屬性的對象,上面的代碼對於屬性的體現其實是方法

counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5

counter.i; // undefined 因為i不是返回對象的屬性
i; // 引用錯誤: i 沒有定義(因為i只存在於閉包)

好的關於這篇文章的內容在此就結束了,如果喜歡的小伙伴請點個贊,你的前進的動力


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM