奋斗30天Javascript之基础语法函數詳解(Part5)

最近又在重新复习js,不怎么用有些就就生疏了,每天都要复习复习在复习才行,分享一下跟着大神学习的日常笔记记录。

有些錯誤可能是沒寫對中英字符,可能會報錯,都手寫的,見諒,废话不多说半句,开始复习吧!

* 语句是javascrpt的基本执行单位,每条语句都是以分号结束,语句除了我们前面讲的赋值语句,算术运算等语句以外,还有一些常用的特殊语句。

1:语句块

{

var name = "taoshiba";

alert("hello:"+name);

}

2:空语块

{

var arr = [];

for(var i=0;i<10;arr[i++]=i+5);

for(var index in arr){

document.write(arr[index]+"<br/>")

}

}

3:异常抛出语句

{

for(var i=0;i<10;i++){

document.write(i+"<br/>");

if(i==3){

throw new Error("报错了");

}

};

}

4:异常捕捉语句

try{

var ago = 3;

if(ago ==3){

throw new Error("年纪太小了");

}

}catch(e){

document.write("出错:"+e.message);

}finally{

document.write("注定会执行的地方");

}

5:with语句

with(document){

write("第一行<br/>");

write("第二行<br/>");

write("第三行<br/>");

}

* Javascript定义函数主要有以下3种方式:

· 命名函数

1-1:命名函数语法

function 函数名(参数列表){

//要执行的语句块

}

1-2:栗子

function hello(age){

alert(age+"岁")

}

hello('20');

· 匿名函数

1-1:匿名函数语法

function(参数列表){

//要执行的语句块

}

1-2:栗子

var ages = function(age){

alert(age+"岁")

}

ages ('20');

· 使用function类构造匿名函数

1-1:function类构造匿名函数语法

new function(参数列表,函数执行体)

1-2:栗子

var age = new function('name','age','alert("名字:"+name+",年纪:"+age)');

age('桃乐丝',20);//window.age('桃乐丝',20)

注意:函数的形参不需要做类型声明,也不要加var,这是javascript属于弱类型语言的一种表现。

大多数人都shis使用第二种匿名函数来定义函数,他的可读性最好。

下面內容是轉載至:https://ithelp.ithome.com.tw/users/20092232/ironman

*  范畴

PS:范畴是指编译器或javascript引擎籍由识别字名称查找变数的一组规则(负责维护变数清单

a:引擎对范畴的查找变数的动作,可分为两种类型:

· LHS(left - hand side):要查找的变数在指定动作的左边,如:a = 3的a在等号的左边,就是执行LHS的查找动作。

· RHS(right- hand side):变数不在指定动作的左边,如:console.log(a),就是执行了RHS的查找动作。

注意:函式宣告(如:function foo(){...})不是LHS,这是因为在做函式宣告时,同时做了宣告和值的定义,而非执行阶段设定其值。

* 巢狀範疇

若在目前執行的範疇找不到這個變數的時候,就會往外層的範疇搜尋,持續搜尋直到找到為止,或直到最外層的全域範疇;而這樣一層又一層的範疇就稱為「巢狀範疇」。

如下,console.log(a + b) 中,b 的 RHS 無法在 foo 中解析完成,但可到全域範疇解析出來。

const foo = (a) => { console.log(a + b); } const b = 2; foo(2); // 4

錯誤(Error)

為什麼需要理解 LHS 和 RHS 呢?這是因為要看懂 JavaScript 報錯的原因。

當解析 identifier 失敗時

  • 若是 RHS,則會丟出 ReferrenceError 的訊息。
  • 若是 LHS,就會分為是否在嚴格模式(strict mode)的情況
    • 在非嚴格模式下,會在全域建立這個變數。
    • 在嚴格模式下,會丟出 ReferrenceError 的訊息。

還有一種狀況,不論在 LHS 和 RHS 下,操作不合法的行為時,就會丟出 TypeError 的訊息。

  • LHS:重新設定已宣告為 const 變數,const a = 2; a = 4; 的 a = 4 會導致 TypeError。
  • RHS:執行不是 function 的變數,const b = 2; b(); 的 b() 會導致 TypeError。

栗子:

這裡有一個小栗子,判斷哪裡發生了 LHS?哪裡發生了 RHS?

const foo = (a) => { const b = a; return a + b; } const c = foo(2);

結果是:

LHS

  1. const c = ...
  2. const b = ...
  3. 隱含的參數設定 a = 2

RHS

  1. const b = a 其中的 ... = a 對 a 取值
  2. return a + b 其中要對 a 取值
  3. return a + b 其中要對 b 取值
  4. foo(a) 其中要對 foo 取得其函數

* 函式範疇

函式會建立自己的範疇,其內的識別字(不管是變數、函式)僅能在這個函式裡面使用。

範例如下,在全域範疇底下,是無法存取 foo 內的 a、b、c 和 bar,否則會導致 ReferrenceError;但在 foo 自己的函式範疇內,可以存取 a、b、c 和 bar。

foo 可自由存取其內的 a、b、c 和 bar。

function foo(a) {

var b = 2;

function bar() { // ... }

var c = 3;

console.log(a); // 2

console.log(b); // 2

console.log(c); // 3

bar(); } foo(2);

全域範疇之下是無法存取 foo 內的 a、b、c 和 bar 的,但可存取 foo 喔!

function foo(a) {

var b = 2;

function bar() { // ... }

var c = 3;

}

foo(2);

console.log(a); // ReferrenceError

console.log(b); // ReferrenceError

console.log(c); // ReferrenceError

bar(); // ReferrenceError

* 使用「函式範疇」有什麼好處呢?或說解決什麼問題呢?大致上有這兩點

  • 維持最小權限原則,以避免變數或函式被不當存取。
  • 避免同名識別字所造成的衝突,這當中包含了避免污染全域命名空間和模組的管理。

最小權限原則

函式範疇能維持「最小權限原則」(principle of least privilege),或稱為「最小授權」(least authority)、「最小暴露」(least exposure),可防止變數或函式被不當存取。

範例如下,secretData 是 foo 的私有變數,可能是儲存了 foo 之外其他程式碼不需要知道的資料,因此對於其他地方(包含全域範疇)的程式碼來說,是無法直接存取到 secretData 的,只能透過 foo 公開的 API「bar」取得經過處理後的資料,如 publicData。這樣的好處是,除了 foo 之外是無法經由任何管道修改它的私有變數 secretData 的,可防止其他地方的程式碼的不當存取。

function foo() {

var secretData = 'HelloWorld';

function bar() {

return secretData.split('').join('-');

}

return { bar }

}

var baz = foo();

var publicData = baz.bar();

console.log(publicData); // H-e-l-l-o-W-o-r-l-d

console.log(secretData); // Uncaught ReferenceError: secretData is not defined

* 避免衝突

避免同名變數或函式所造成的衝突。

如下範例,這裡有兩個函式 doSomething 與 doSomethingElse。

function doSomething(a) {

b = a + doSomethingElse(a * 2);

console.log(b * 3);

}

function doSomethingElse(a) {

return a - 1;

}

var b; doSomething(2); // 15

若此時還有一個同名的函式 doSomethingElse,就會導致衝突,回傳的答案就不是原本預期的 15,而是 12。

function doSomething(a) {

b = a + doSomethingElse(a * 2);

console.log(b * 3);

}

function doSomethingElse(a) {

return a - 1;

}

var b; doSomething(2); // 12

function doSomethingElse(a) {

return a - 2;

}

改寫如下,將 doSomething 私有的細節(也就是第一個 doSomethingElse 函式)藏在其範疇中,這樣兩個 doSomethingElse 函式就不會造成衝突了。

function doSomething(a) {

var b; b = a + doSomethingElse(a * 2);

console.log(b * 3);

function doSomethingElse(a) {

return a - 1;

}

}

doSomething(2); // 15

function doSomethingElse(a) { return a - 2; }

再看一個例子,如下,函式 bar 內的 i 是個全域變數,它無意間修改的 for loop 的 i,導致 i 永遠都是 3,而進入了無窮迴圈。

function foo() {

function bar(a) {

i = 3; console.log(a + i);

}

for (var i = 0; i < 10; i++) { bar(i * 2); } } foo();

解法是將 bar 內的 i 宣告為區域變數,這樣就會將這個 i 包在 bar 的範疇裡面,避免被其他不相干的程式碼存取。

function foo() {

function bar(a) {

var i = 3; // 將 bar 內的 i 宣告為區域變數

console.log(a + i);

}

for (var i = 0; i < 10; i++) {

bar(i * 2);

}

}

foo();

* 全域命名空間(Global Namespace)

通常我們使用的函式庫都會適當的隱藏自己內部所使用的變數和函式,意即將它們做成某物件的屬性和方法而非暴露在全域底下,而物件即是它們的命名空間(namespace),這樣就可以避免在全域範疇中因同名而產生的衝突。

範例如下,物件 MyReallyCoolLibrary 內含有屬性 awesome 和方法 doSomething 與 doAnotherThing,可避免全域範疇中也有同名的變數 awesome 或函式 doSomething 或 doAnotherThing。

var MyReallyCoolLibrary = {

awesome: 'stuff',

doSomething: function() { // ... },

doAnotherThing: function() { // ... }

};

* 即刻調用函式運算式(Immediately Invoked Function Expression, IIFE)

IIFE 是可立即執行的函式運算式,主要好處是不污染全域範疇,並且匿名或具名皆合法。

在談論 IIFE 前,先來看幾個重要觀念

  • 函式宣告 vs 函式運算式
  • 匿名 vs 具名

函式宣告(Function Declaration)vs 函式運算式(Function Expression)

函式宣告(function declaration)就像是其他資料型別所宣告的字面值一樣,利用關鍵字 function 宣告一個函式,後接函式名稱與其本體,範例如下。

function foo() {

var a = 3;

console.log(a); // 3

}

foo();

函式運算式(function expression)是指將一個函式指定給特定變數的過程,範例如下。

var foo = function bar() { var a = 3; console.log(a); // 3 } foo();

廣義上來說,只要函式述句並非以 function 開頭,而是以 var foo = function ... 或 (function foo() ... 起始的(像是稍後提到的 IIFE),都是函式運算式。

* 匿名 vs 具名

承上,函式運算式可分為具名和匿名的,範例如下。

具名的函式運算式,具有名稱識別字 bar。

var foo = function bar() {

var a = 3;

console.log(a); // 3

}

foo();

匿名的函式運算式,匿名就沒有名稱識別字。

var foo = function() {

var a = 3;

console.log(a); // 3

}

foo();

再看另一個例子,我們很習慣在 callback 中使用匿名運算式。這好嗎?

setTimeout(function() { console.log('等一秒後執行'); }, 1000);

或寫成 arrow function

setTimeout(() => { console.log('等一秒後執行'); }, 1000);

而匿名的函式運算式有以下缺點

  • stack trace 因報錯時沒有具體名稱會較難追蹤。
  • 沒有名稱會難以遞迴(解法是必須使用已廢棄的 arguments.callee),且無法指定名稱做自身的 unbind。
  • 無法立即知道該匿名函式的功能,可讀性較差。

解法就是給它一個名字,例如 timeoutHandler,百利而無一害,用吧。

setTimeout(function timeoutHandler() { console.log('等一秒後執行'); }, 1000);

const timeoutHandler = () => { console.log('等一秒後執行'); } setTimeout(timeoutHandler, 1000);

先前提到的例子中,不管是函式宣告或函式運算式,都會污染到全域範疇,因此可能會遇到剛才所提到的問題...像是避免變數或函式被不當存取、同名識別字所造成的衝突等。因此,我們可使用「即刻調用函式運算式」(Immediately Invoked Function Expression, IIFE)來解決這個問題。

IIFE 是可立即執行的函式運算式,主要好處是不污染全域範疇,並且匿名或具名皆合法。

具名為 foo 的 IIFE。

(function foo(){

var a = 3;

console.log( a ); // 3

})();

foo(); // foo is not defined

匿名的 IIFE。

(function() {

var a = 3;

console.log(a); // 3

})();

IIFE 還有一些功能,例如:指定範疇、確保 undefined 的正確性與反轉順序。

* 指定範疇

將傳入的參數當作範疇。

如下,將 window 傳入以作為具名的 IIFE 的範疇,並指名為 global,這樣的命名方式有助於程式的可讀性,簡單易懂。

var a = 2;

(function IIFE(global) {

var a = 3;

console.log(a); // 3

console.log(global.a); // 2

})(window); console.log(a); // 2

確保 undefined 的正確性

有些程式碼會因為錯誤的撰寫方式,導致污染了 undefined 的值,因此可指定一個參數,但不傳入值,以維持undefined 的正確性。

如下,IIFE 雖然有設定參數 undefined,但 () 卻是空的。

undefined = true; (function IIFE(undefined) { var a; if (a === undefined) { console.log('Undefined 在這裡很安全!'); } })();

反轉順序

前方放置呼叫的參數並執行未來傳入的函式,而後方放置將要執行的函式。這種寫法常用於 UMD(universal module definition)。

var a = 2; (function IIFE(def) {

def(window);

})

(function def(global) {

var a = 3;

console.log(a); // 3

console.log(global.a); // 2

});

把上面這段程式碼拆開來,可當成這裡有兩個變數 a 和 def,其中 def 是待會要執行的函式。

var a = 2; var def = function(global) {

var a = 3;

console.log(a); // 3

console.log(global.a); // 2

};

使用 IIFE 結構,前方將要執行的函式當成參數 func 傳入,並且 func 代入 window 這個參數。接著,由後方傳入要執行的函式 def。

(function IIFE(func) { func(window); })(def);

不過呢,自從有了 ES6 的 let 與 var 搭配區塊範疇 {...} 之後,我們再也不需要 IIFE 了。

(function foo(){ var a = 3; console.log( a ); // 3 })(); foo(); // Uncaught ReferenceError: foo is not defined

剛剛的例子就可以改成...

{ const foo = () => { let a = 3; // 做一些運算... console.log(a); }; } foo(); // Uncaught ReferenceError: foo is not defined

* 區塊範疇(Block Scope)

在 ES6 以前,只有函式能建立範疇,而在 ES6 之後,可用大括號 { ... } 定義區塊範疇,讓 const 和 let 宣告以區塊為範疇的變數。

如下,i 屬於函式 foo 的範疇,而非假想的 for loop 的區塊範疇。

function foo() { for(var i = 0; i < 10; i++) { console.log(i); } }

而 ES6 的 const 與 let 可宣告以區塊為範疇的變數。

const。

var foo = true; if (foo) { const bar = foo * 2; console.log(bar); // 2 } console.log(bar); // ReferenceError

const 表示常數(constant),宣告時就必須賦值,賦值後不可修改其值。

const bar = foo * 2; bar = 3; // Uncaught TypeError: Assignment to constant variable.

let。

var foo = true; if (foo) { let bar = foo * 2; console.log(bar); // 2 } console.log(bar); // ReferenceError

當 let 宣告於 for 迴圈內時...

for (let i = 0; i < 10; i++) {

console.log(i);

}

console.log(i); // ReferenceError: i is not defined

上面這段程式碼可以看成是這樣...i 是屬於第一個大括號所包含的區塊的,因此 i 一但出了第一個大括號所包含的範圍就會報錯。

{ let i; for (i = 0; i < 10; i++) { console.log(i); } } console.log(i); // ReferenceError: i is not defined

注意,迴圈的每次迭代都會對 i 重新綁定(rebind),這樣就能確保重新賦值。

const 與 let 不會有拉升(hoisting)的狀況。

if (foo) { console.log(bar); // ReferenceError let bar = foo * 2; }

* 垃圾回收(Garbage Collection)

一但變數用不到了,JavaScript 引擎就可能會將它回收,但由於範疇的緣故,仍須保留這些變數存取值的能力,而區塊範疇明確表達資料不再用到,而解決這個不需要被保留的狀況,可釋出更多記憶體空間。這部份與閉包(closure)有關,待後續詳細說明閉包的機制。

範例如下,雖然 clickHandler 用不到變數 someReallyBigData,因此函式 process 處理完 someReallyBigData 應該就可回收 someReallyBigData 的記憶體空間,但由於 clickHandler 擁有對整個範疇的閉包(後續會提到,閉包是函式記得並存取語彙範疇的能力,可說是指向特定範疇的參考,因此當函式是在其語彙範疇之外執行時也能正常運作),因此 JavaScript 就不會把它回收了。

function process(data) {

// 做一些有趣的事情...

}

var someReallyBigData = { .. };

process(someReallyBigData);

var btn = document.getElementById('this_button');

btn.addEventListener('click'), function clickHandler(e){

console.log('按鈕按下去了');

});

但是呢,區塊範疇能幫我們解決這個問題,區塊範疇會告訴 JavaScript 引擎這些內容僅在這一塊範圍內用到而已,之後就能讓 JavaScript 引擎順利地把用不到的資料回收掉。

function process(data) {

// 做一些有趣的事情...

} // 在這個區塊內宣告的任何資料在處理完後就可被丟棄! {

let someReallyBigData = { .. };

process(someReallyBigData);

}

var btn = document.getElementById('this_button');

btn.addEventListener('click'), function clickHandler(e){

console.log('按鈕按下去了');

});

猜你喜欢

转载自blog.csdn.net/weixin_41406727/article/details/88429918