js學習筆記(7):函數

函數聲明的兩種方法

  • function命令聲明。function命令後面是函數名,函數名後面是一對圓括號,裏面是傳入函數的參數。函數體放在大括號裏面。
function print(s) {
  console.log(s);
}
  • 變量賦值寫法。採用函數表達式聲明函數時,function命令後面通常不帶有函數名。若是加上函數名,該函數名只在函數體內部有效,在函數體外部無效。
var print = function x(){
  console.log(typeof x);
};

x
// ReferenceError: x is not defined

print()
// function

可是,咱們在函數表達式中,每每會加入這個函數名x。雖然這個x只在函數體內部可用,指代函數表達式自己,其餘地方都不可用。可是這種寫法有兩個好處:(1)能夠在函數體內部調用自身,方便遞歸;(2)方便除錯(除錯工具顯示函數調用棧時,將顯示函數名,而再也不顯示這裏是一個匿名函數)編程

  • function命令聲明,尾部不須要加分號;函數表達式尾部要加分號

函數的重複聲明

若一個函數被屢次聲明,後邊的聲明會覆蓋前邊的聲明(這裏要注意的是,因爲函數名的提高,前一次聲明在任什麼時候候都是無效的,並非在第二次聲明前還有效,而是所有覆蓋)數組

function f() {
  console.log(1);
}
f() // 2

function f() {
  console.log(2);
}
f() // 2

第一等公民

  • js將函數看做一種值,與其餘值(數值、字符串、布爾值)地位相同,凡是可使用值的地方,就能使用函數。例如把函數賦值給變量和對象的屬性,當作參數傳入其餘函數,做爲函數結果返回等。函數只是一個能夠執行的值,此外並沒有特殊之處。

因爲函數與其它數據類型地位平等,因此js中又稱函數爲第一等公民。安全

function add(x, y) {
  return x + y;
}

// 將函數賦值給一個變量
var operator = add;

// 將函數做爲參數和返回值
function a(op){
  return op;
}
a(add)(1, 1)
// 2

函數名的提高

因爲js引擎將函數名視同變量名,因此也遵循變量聲明的原則,會被提高到代碼頭部閉包

所以,此代碼不報錯函數

f();

function f() {}

可是,若採用賦值語句定義函數,就會發生錯誤。這與變量名聲明提早相似,只是將函數聲明提早,而沒有將函數的賦值提早工具

f();
var f = function (){};
// TypeError: undefined is not a function
var f;
f();
f = function () {};

再看下這個例子性能

var f = function () {
  console.log('1');
}

function f() {
  console.log('2');
}

f() // 1

先var f,而後第二個函數聲明覆蓋了第一個函數聲明,接着第一個函數的賦值覆蓋了第二個函數的聲明,最後仍是第一個函數的賦值優化

函數的屬性和方法

  • name屬性返回函數的名字

當變量賦值定義函數時,若爲匿名函數,返回變量名;如有具體函數名,返回function後的函數名prototype

var f3 = function myName() {};
f3.name // 'myName'

name屬性的重要做用,就是獲取傳入的函數參數的函數名code

var myFunc = function () {};

function test(f) {
  console.log(f.name);
}

test(myFunc) // myFunc
  • length屬性返回函數預期傳入的參數個數。提供了一種機制,判判定義時和調用時參數的差別,以便實現面向對象編程的「方法重載」(overload)。
  • toString()方法返回一個字符串,內容是函數的源碼。值得注意的是,當函數爲原生函數時,返回function (){[native code]};能夠返回函數內部註釋,而且能夠藉助這一點變相實現多行字符
var multiline = function (fn) {
  var arr = fn.toString().split('\n');
  return arr.slice(1, arr.length - 1).join('\n');
};

function f() {/*
  這是一個
  多行註釋
*/}

multiline(f);
// " 這是一個
//   多行註釋"

函數做用域

概述

js只有兩種做用域:(1)全局做用域:在函數外部聲明;(2)函數做用域:在函數內部定義,外部沒法讀取

值得注意的是,函數內部定義的變量,會在該做用域覆蓋掉同名全局變量

另外,只有在函數內部聲明的纔是局部變量,在其餘區塊中聲明,儘管和函數同樣都有區塊,一概是全局變量

函數內部的變量提高

與全局做用域同樣,函數做用域內部也會產生「變量提高」現象。var命令聲明的變量,無論在什麼位置,變量聲明都會被提高到函數體的頭部。

function foo(x) {
  if (x > 100) {
    var tmp = x - 100;
  }
}

// 等同於
function foo(x) {
  var tmp;
  if (x > 100) {
    tmp = x - 100;
  };
}
函數自己的做用域

在js中,函數是一等公民,與值相同,所以也有本身的做用域,其做用域與變量同樣,就是其聲明時所在的做用域,與運行時所在的做用域無關。

var a = 1;
var x = function () {
  console.log(a);
};

function f() {
  var a = 2;
  x();
}

f() // 1

上面代碼中,函數x是在函數f的外部聲明的,因此它的做用域綁定外層,內部變量a不會到函數f體內取值,因此輸出1,而不是2

總之,函數執行時所在的做用域,是定義時的做用域,而不是調用時所在的做用域。

很容易犯錯的一點是,若是函數A調用函數B,卻沒考慮到函數B不會引用函數A的內部變量。

var x = function () {
  console.log(a);
};

function y(f) {
  var a = 2;
  f();
}

y(x)
// ReferenceError: a is not defined

上面代碼將函數x做爲參數,傳入函數y。可是,函數x是在函數y體外聲明的,做用域綁定外層,所以找不到函數y的內部變量a,致使報錯。

一樣的,函數體內部聲明的函數,做用域綁定函數體內部。

function foo() {
  var x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}

var x = 2;
var f = foo();
f() // 1

上面代碼中,函數foo內部聲明瞭一個函數barbar的做用域綁定foo。當咱們在foo外部取出bar執行時,變量x指向的是foo內部的x,而不是foo外部的x。正是這種機制,構成了下文要講解的「閉包」現象。

參數

首先注意,參數是能夠省略的。不管函數定義時定義了多少參數,可是運行時提供多少參數甚至不提供參數,js都不會報錯。省略的參數的值就變爲undefined。須要注意的是,函數的length屬性與實際傳入的參數個數無關,只反映函數預期傳入的參數個數。

function f(a, b) {
  return a;
}

f(1, 2, 3) // 1
f(1) // 1
f() // undefined

f.length // 2

可是,沒有辦法只省略靠前的參數,而保留靠後的參數。若是必定要省略靠前的參數,只有顯式傳入undefined

function f(a, b) {
  return a;
}

f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined

上面代碼中,若是省略第一個參數,就會報錯

傳遞方式
  • 若參數是原始類型的值(數值、字符串、布爾值),那麼傳遞方式是傳值傳遞。即將值拷貝到函數中進行操做。這時在函數體內修改參數值,不會影響到函數外部的原始值
var p = 2;

function f(p) {
  p = 3;
}
f(p);

p // 2
  • 可是,若參數是複合類型的值(數組、對象、其餘函數),那麼傳入方式是傳址傳遞。即將參數的地址傳到函數中進行操做,這時在函數內部修改參數,將會影響到原始值。
var obj = { p: 1 };

function f(o) {
  o.p = 2;
}
f(obj);

obj.p // 2
同名參數

如有同名的參數,則取最後出現的那個值,即便後邊的參數沒有值或者被省略,也是以其爲準

function f(a, a) {
  console.log(a);
}

f(1) // undefined

這時,若要得到第一個a的值,可使用arguments對象

function f(a, a) {
  console.log(arguments[0]);
}

f(1) // 1

arguments對象

arguments對象包含了函數運行時的全部參數,arguments[0]就是第一個參數,arguments[1]就是第二個參數,以此類推。這個對象只有在函數體內部,纔可使用。

var f = function (one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}

f(1, 2, 3)
// 1
// 2
// 3

通常狀況下,arguments對象能夠在運行時修改,例如,函數f()調用時傳入的參數,在函數內部被修改爲32

var f = function(a, b) {
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 5

可是若使用嚴格模式,修改arguments對象就不會影響到實際的函數參數。

var f = function(a, b) {
  'use strict'; // 開啓嚴格模式
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 2

經過arguments對象的length屬性,能夠判斷函數調用時到底帶幾個參數。

function f() {
  return arguments.length;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0
與數組的關係

值得注意的是,雖然arguments很像數組,但它是一個對象。數組專有的方法(好比sliceforEach),不能在arguments對象上直接使用。

若是要讓arguments對象使用數組方法,真正的解決方法是將arguments轉爲真正的數組。下面是兩種經常使用的轉換方法:slice方法和逐一填入新數組。

var args = Array.prototype.slice.call(arguments);

// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}

閉包

js語言特有鏈式做用域結構,子對象會一級一級的向上尋找全部父對象的變量。即全部父對象的變量對子對象都是可見的,可是子對象的變量對父對象不可見(老父親了)

例如,函數內部能夠直接讀取全局變量,函數外部卻沒法讀取函數內部的變量;F2能夠讀取F1的變量,F1卻不能讀取F2的變量

function f1() {
  var n = 999;
  function f2() {
  console.log(n); // 999
  }
}

下邊咱們思考一個問題,原本f1外部是讀取不到f1的變量的,可是f2能夠讀取到父對象f1的變量,若咱們把f2做爲返回值,不就能夠在f1外部讀取f1的內部變量了嗎!

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

這就是閉包的概念,簡單來講,閉包就是定義在函數內部的函數,可以讀取父函數內部變量的f2.本質上講,閉包可以記住誕生的環境,是將函數內部和函數外部連接起來的一座橋樑

閉包的做用
  • 讀取函數內部的變量
  • 使得誕生環境一直存在,使函數內部變量始終保存在內存中。以下,閉包使得內部變量記住上一次調用時的運算結果。

start是函數createIncrementor的內部變量。經過閉包,start的狀態被保留了,每一次調用都是在上一次調用的基礎上進行計算。從中能夠看到,閉包inc使得函數createIncrementor的內部環境,一直存在。因此,閉包能夠看做是函數內部做用域的一個接口。

爲何會這樣呢?緣由就在於inc始終在內存中,而inc的存在依賴於createIncrementor,所以也始終在內存中,不會在調用結束後,被垃圾回收機制回收。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7
  • 封裝對象的私有屬性和私有方法
function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }

  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}

var p1 = Person('張三');
p1.setAge(25);
p1.getAge() // 25

上面代碼中,函數Person的內部變量_age,經過閉包getAgesetAge,變成了返回對象p1的私有變量。

  • 注意,外層函數每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數的內部變量,因此內存消耗很大。所以不能濫用閉包,不然會形成網頁的性能問題。

IIFE:當即調用的函數表達式

在js 中,圓括號()是一種運算符,跟在函數名以後,表示調用該函數。好比,print()就表示調用print函數。

可是,當咱們在定義函數後當即調用函數時,不能定義後加括號,不然會產生語法錯誤。這是由於function便可以當作語句,也能夠當作表達式,而js引擎規定,若function出如今句首,一概解釋爲語句。因此,JavaScript 引擎看到行首是function關鍵字以後,認爲這一段都是函數的定義,不該該以圓括號結尾,因此就報錯了。

// 語句
function f() {}

// 表達式
var f = function f() {}

解決方法也很簡單,就是將函數放到一個圓括號裏。這就是IIFE(當即調用的函數表達式)

值得注意的是,最後的分號是必須的,不然會報錯

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
做用

一般狀況下,只對匿名函數使用這種「當即執行的函數表達式」。它的目的有兩個:一是沒必要爲函數命名,避免了污染全局變量;二是 IIFE 內部造成了一個單獨的做用域,能夠封裝一些外部沒法讀取的私有變量。

// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 寫法二
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

上面代碼中,寫法二比寫法一更好,由於徹底避免了污染全局變量。

eval命令

eval命令接受一個字符串做爲參數,並將這個字符串看成語句執行。

eval('var a = 1;');
a // 1

若是參數字符串沒法看成語句運行,那麼就會報錯。

eval('3x') // Uncaught SyntaxError: Invalid or unexpected token

放在eval中的字符串,應該有獨自存在的意義,不能用來與eval之外的命令配合使用。舉例來講,下面的代碼將會報錯。

eval('return;'); // Uncaught SyntaxError: Illegal return statement

上面代碼會報錯,由於return不能單獨使用,必須在函數中使用。

若是eval的參數不是字符串,那麼會原樣返回。

eval(123) // 123

eval沒有本身的做用域,都在當前做用域內執行,所以可能會修改當前做用域的變量的值,形成安全問題。

var a = 1;
eval('a = 2');

a // 2

上面代碼中,eval命令修改了外部變量a的值。因爲這個緣由,eval有安全風險。

爲了防止這種風險,JavaScript 規定,若是使用嚴格模式,eval內部聲明的變量,不會影響到外部做用域。

(function f() {
  'use strict';
  eval('var foo = 123');
  console.log(foo);  // ReferenceError: foo is not defined
})()

上面代碼中,函數f內部是嚴格模式,這時eval內部聲明的foo變量,就不會影響到外部。

不過,即便在嚴格模式下,eval依然能夠讀寫當前做用域的變量。

(function f() {
  'use strict';
  var foo = 1;
  eval('foo = 2');
  console.log(foo);  // 2
})()

上面代碼中,嚴格模式下,eval內部仍是改寫了外部變量,可見安全風險依然存在。

總之,eval的本質是在當前做用域之中,注入代碼。因爲安全風險和不利於 JavaScript 引擎優化執行速度,因此通常不推薦使用。一般狀況下,eval最多見的場合是解析 JSON 數據的字符串,不過正確的作法應該是使用原生的JSON.parse方法。

eval 的別名調用

前面說過eval不利於引擎優化執行速度。更麻煩的是,還有下面這種狀況,引擎在靜態代碼分析的階段,根本沒法分辨執行的是eval

var m = eval;
m('var x = 1');
x // 1

上面代碼中,變量meval的別名。靜態代碼分析階段,引擎分辨不出m('var x = 1')執行的是eval命令。

爲了保證eval的別名不影響代碼優化,JavaScript 的標準規定,凡是使用別名執行evaleval內部一概是全局做用域。

var a = 1;

function f() {
  var a = 2;
  var e = eval;
  e('console.log(a)');
}

f() // 1

上面代碼中,eval是別名調用,因此即便它是在函數中,它的做用域仍是全局做用域,所以輸出的a爲全局變量。這樣的話,引擎就能確認e()不會對當前的函數做用域產生影響,優化的時候就能夠把這一行排除掉。

eval的別名調用的形式五花八門,只要不是直接調用,都屬於別名調用,由於引擎只能分辨eval()這一種形式是直接調用。

eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')

上面這些形式都是eval的別名調用,做用域都是全局做用域。

相關文章
相關標籤/搜索