重讀javascript之Function

函數包含一組語句,它們是JavaScript的基礎模塊單元,用於代碼的複用、信息隱藏和組合調用。函數用於指定對象的行爲。通常來講,所謂編程,就是將一組需求分解成函數與數據結構的技能。前端

JavaScript中函數被做爲「一等公民」,函數也屬於對象,不一樣的是隻有函數能夠被調用。es6

函數聲明與表達式

函數聲明

function foo() {}

上面的方法會在執行前被 解析(hoisted),所以它存在於當前上下文的任意一個地方, 即便在函數定義體的上面被調用也是對的。算法

foo(); // 正常運行,由於foo在代碼運行前已經被建立
function foo() {}

函數賦值表達式

var foo = function() {};

這個例子把一個匿名的函數賦值給變量 foo。編程

foo; // 'undefined'
foo(); // 出錯:TypeError
var foo = function() {};

因爲 var 定義了一個聲明語句,對變量 foo 的解析是在代碼運行以前,所以 foo 變量在代碼運行時已經被定義過了。數組

可是因爲賦值語句只在運行時執行,所以在相應代碼執行以前, foo 的值缺省爲 undefined瀏覽器

命名函數的賦值表達式

另一個特殊的狀況是將命名函數賦值給一個變量。緩存

var foo = function bar() {
    bar(); // 正常運行
}
bar(); // 出錯:ReferenceError

bar 函數聲明外是不可見的,這是由於咱們已經把函數賦值給了 foo; 然而在 bar 內部依然可見。這是因爲 JavaScript 的 命名處理 所致, 函數名在函數內老是可見的。[注意]:在IE8及IE8如下版本瀏覽器bar在外部也是可見的,是由於瀏覽器對命名函數賦值表達式進行了錯誤的解析, 解析成兩個函數 foo 和 bar.數據結構

函數的調用

調用一個函數會暫停當前函數的執行,傳遞控制權和參數給新的函數。除了聲明時定義的形式參數外,還傳遞兩個隱式的參數:this和arguments.this的值取決於調用模式(方法調用,函數調用,構造函數調用,apply調用)。當實際參數和形式參數不匹配時不會報錯,若是實際參數大於形式參數,多的值會忽略。若是實際參數小於形式參數,多的形式參數會設undefine.閉包

方法調用

var Obj = {
    value: 0,
    increment: function(inc){
        this.value += typeof inc === "number" ? inc : 1; 
    }
};
Obj.increment();  // 1
Obj.increment(2); //3

方法可使用this訪問本身所屬的對象,因此它能從對象中取值和對對象進行修改,this到對象的綁定發生在調用的時候。app

函數調用

//給Obj加一個double方法
Obj.double = function(){
    var add = function(){
        var val = this.value;
        if(typeof val === "number"){
            this.value = val * 2;
        }
    }
    add();
}
Obj.double();

以上代碼達不到目的,由於以此模式調用時,this被綁定到了全局變量。這是語言設計上的一個錯誤,假若語言設計正確,那麼當內部函數被調用時,this應該綁定到外部函數的this變量。這個設計錯誤的後果就是方法不能利用內部函數來幫助它工做,由於內部函數的this被綁定了錯誤的值,因此不能共享該方法對對象的訪問權。幸運的是,有一個很容易的解決方案:若是該方法定義了一個變量並給他賦值this,那麼內部函數就能夠經過那個變量訪問到this. 按照約定,咱們能夠把那個變量命名that:

//給Obj加一個double方法
Obj.double = function(){
    var that = this;
    var add = function(){
        var val = that.value;
        if(typeof val === "number"){
            that.value = val * 2;
        }
    }
    add();
}
Obj.double();

構造函數調用

若是在一個函數前面帶上new 來調用,那麼背地裏將會建立一個鏈接到該函數的prototype成員的新對象,同時this會綁定到那個對象上。new前綴也改變了return語句的行爲。

var Obj = function(val){
    this.value = val;
}
var myObj = new Obj();

Apply調用模式

apply方法讓我門構建一個參數數組傳遞給調用函數。它也容許選擇this的值。相似的還有call.

Object.prototype.toString.call({}); //"[object Object]"

屬性

prototype

Function.prototype 屬性存儲了構造函數的原型對象。可使用該屬性實現繼承:

function Animal(name){
    this.name = name;
}
function Bird(){
}
bird.porototype = new Animal(); //bird的原型指向Animal的原型,繼承Animal的屬性。

這個屬性和對象的內部屬性[[prototype]]是有所不一樣的:

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z;
  }
};
var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});

以上實際是經過對象的內部屬性[[prototype]]實現繼承。
對象繼承

function Foo(y) {
  this.y = y;
}
 
Foo.prototype.x = 10;
 
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

var b = new Foo(20);
var c = new Foo(30);

用構造函數實現繼承,構造函數的原型鏈以下圖:
構造函數原型鏈

arguments

function.arguments 已經被廢棄不少年了,如今推薦的作法是使用函數內部可用的 arguments 對象來訪問函數的實參。因此主要講述一下arguments的特性:arguments 是一個類數組對象。表明傳給一個function的參數列表。,arguments 對象是函數內部的本地變量;arguments 已經再也不是函數的屬性了。arguments 對象並非一個真正的Array。它相似於數組,但沒有數組所特有的屬性和方法,除了 length。例如,它沒有 pop 方法。不過能夠將其轉換成數組。

length

length 屬性指明函數的形參個數。數量不包括剩餘參數[ES6]。相比之下, arguments.length 是函數被調用時實際傳參的個數。

非標準屬性

  • name: name 屬性返回所屬函數的函數名。name 屬性返回一個函數的名稱, 若是是匿名函數, 則返回空字符串。

  • caller: 返回調用指定函數的函數。若是一個函數f是在全局做用域內被調用的,則f.caller爲null,相反,若是一個函數是在另一個函數做用域內被調用的,則f.caller指向調用它的那個函數。

  • displayName: 獲取函數的顯示名字。

方法

Function.prototype.bind()方法

bind()方法會建立一個新函數,稱爲綁定函數,當調用這個綁定函數時,綁定函數會以建立它時傳入 bind()方法的第一個參數做爲 this,傳入 bind() 方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。

example

在下面的例子代碼中,咱們能夠名正言順地將上下文緩存到一個變量中:

var myObj = {
 
    specialFunction: function () {
 
    },
 
    anotherSpecialFunction: function () {
 
    },
 
    getAsyncData: function (cb) {
        cb();
    },
 
    render: function () {
        var that = this;
        this.getAsyncData(function () {
            that.specialFunction();
            that.anotherSpecialFunction();
        });
    }
};
 
myObj.render();

咱們須要爲回調函數的執行保持對 myObj 對象上下文的引用。 調用 that.specialFunction()讓咱們可以維持做用域上下文而且正確執行咱們的函數。 然而使用 Function.prototype.bind() 能夠有更加簡潔乾淨的方式:

render: function () {
 
    this.getAsyncData(function () {
 
        this.specialFunction();
 
        this.anotherSpecialFunction();
 
    }.bind(this));
 
}

ecma-262規範:

When the bind method is called with argument thisArg and zero or more args, it performs the following steps:

  • Let Target be the this value.

  • If IsCallable(Target) is false, throw a TypeError exception.

  • Let args be a new (possibly empty) List consisting of all of the argument values provided after thisArg in order.

  • Let F be BoundFunctionCreate(Target, thisArg, args).

  • ReturnIfAbrupt(F).

  • Let targetHasLength be HasOwnProperty(Target, "length").

  • ReturnIfAbrupt(targetHasLength).

  • If targetHasLength is true, then

    • Let targetLen be Get(Target, "length").

    • ReturnIfAbrupt(targetLen).

    • If Type(targetLen) is not Number, let L be 0.

    • Else,

      • Let targetLen be ToInteger(targetLen).

      • Let L be the larger of 0 and the result of targetLen minus the number of elements of args.

    • Else let L be 0.

    • Let status be DefinePropertyOrThrow(F, "length", PropertyDescriptor {[[Value]]: L, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true}).

    • Assert: status is not an abrupt completion.

    • Let targetName be Get(Target, "name").

    • ReturnIfAbrupt(targetName).

    • If Type(targetName) is not String, let targetName be the empty string.

    • Perform SetFunctionName(F, targetName, "bound").

    • Return F.

Function.prototype.bind 在IE8及如下的版本中不被支持,如下MDN兼容舊瀏覽器的實現:

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, 
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP && oThis
                                 ? this
                                 : oThis || window,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

上述算法和實際的實現算法還有許多其餘的不一樣。

Function.prototype.apply()

apply() 方法在指定 this 值和參數(參數以數組或類數組對象的形式存在)的狀況下調用某個函數。

/* min/max number in an array */
var numbers = [5, 6, 2, 3, 7];

/* using Math.min/Math.max apply */
var max = Math.max.apply(null, numbers); /* This about equal to Math.max(numbers[0], ...) or Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);

Function.prototype.call()

call() 方法在使用一個指定的this值和若干個指定的參數值的前提下調用某個函數或方法。該方法的做用和 apply() 方法相似,只有一個區別,就是call()方法接受的是若干個參數的列表,而apply()方法接受的是一個包含多個參數的數組。

Function.prototype.toString()

該 toString() 方法返回一個表示當前函數源代碼的字符串。Function 對象覆蓋了從 Object 繼承來的 Object.prototype.toString 方法。函數的 toString 方法會返回一個表示函數源代碼的字符串。具體來講,包括 function關鍵字,形參列表,大括號,以及函數體中的內容。

非標準方法

  • Function.prototype.isGenerator(): 斷一個函數是不是一個生成器.

  • Function.prototype.toSource(): 返回函數的源代碼的字符串表示。

閉包

做用域鏈

函數建立時,當一個函數建立後,它的做用域鏈會被建立此函數的做用域中可訪問的數據對象填充。它的做用域鏈中會填入一個全局對象,該全局對象包含了全部全局變量。

函數執行時,會建立一個被稱爲「運行期上下文」內部對象,運行期上下文定義了函數的執行環境。每一個運行期上下文都有本身的做用域鏈,用於標識符的解析。當運行期上下文被建立時,它的做用域鏈會被初始化爲當前運行函數的[[scope]]所包含的對象。

這些值按照它們出如今函數中的順序被複制到運行期上下文的做用域鏈中。它們共同組成了一個新的對象,叫「活動對象(activation object)」,該對象包含了函數的全部局部變量、命名參數、參數集合以及this,而後此對象會被推入做用域鏈的前端,當運行期上下文被銷燬,活動對象也隨之銷燬。[注:]內部函數的執行上下文中this指向全局變量,見方法調用。

閉包的概念

一個擁有許多變量和綁定了這些變量的環境的表達式(一般是一個函數)。能夠讀取函數外部的變量,讓這些執行環境始終保持在內存中。

function test(){
    for(var i = 0; i < 10 ; i++){
        setTimeout(function(){
            console.log(i)
        }, 0);
    }
}
test(); // 10...10(10個)

以上函數本來想輸出0到9,結果輸出了10個10。緣由是test()執行時會建立一個運行時期的上下文,而setTimeout內部的函數會放在for循環隊列以後,等到for循環執行完以後纔開始執行。function(){console.log(i)}執行是首先會尋找函數內部的i標示符。此時找不到i,再尋找test中的i(閉包的概念:訪問函數外的變量,這些變量只有等到閉包不使用纔會被銷燬),而此時的i值已經變爲了10,因此十次執行都會輸出10。解決這個問題能夠在做用域鏈上增長一個節點,保存i變量。

function test(){
    for(var i = 0; i < 10 ; i++){
        (function(li){
            setTimeout(function(){
                console.log(li)
            }, 0);
        })(i);
    }
}
test(); // 0...9(0到9)

經過增長一個變量保存每次執行須要輸出的i值,實現0到9的輸出。

閉包的運用

JavaScript在es6以前沒有模塊的概念,使用閉包和匿名自執行函數實現模塊化,使用閉包能夠從外部放問函數內部的屬性:

(function (){
    //內部屬性
    var Number = 0;
    //方法
    var Utils = function(){};
    Util.porototype.display = function(){
        console.log(Number); //訪問函數外部的變量
    };
    //返回
    return Util;
})()
相關文章
相關標籤/搜索