[ JavaScript ] 函數

1. 函數的定義及調用

1.1 定義函數

function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

function: 定義函數關鍵字;
abs: 函數名
(x): x 爲函數的參數,多個參數以 , 分隔
{ ... }: 之間的代碼塊是函數體

 

函數體內部的語句在執行時,一旦執行到 return 時,函數就執行完畢,並將結果返回。若是沒有 return 語句,函數執行完畢後也會返回結果,只有結果爲 undefinedjavascript

 

第二種定義函數的方式:java

var abs = function (x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
};

 

這兩種定義徹底等價,注意第二種方式按照完整語法須要在函數體末尾加一個 ;  表示賦值語句結束。面試

 

 1.2 調用函數

調用函數時,按順序傳入參數便可:數組

 

 因爲JavaScript容許傳入任意個參數而不影響調用,所以傳入的參數比定義的參數多也沒有問題,雖然函數內部並不須要這些參數:瀏覽器

abs1(10, 'acb', 'xiaoming');
10

 傳入的參數比定義的少也沒有問題:bash

abs1();
NaN

 當函數要求傳入參數,調用時沒有傳參,此時 abs1(x) 函數的參數 x 將收到 undefined,計算結果爲 NaN閉包

要避免收到 undefined,能夠對參數進行檢查:app

 

arguments函數

 這個關鍵字只能在當前函數內部使用,做用是獲取函數調用者傳入的全部參數。測試

 

利用arguments,你能夠得到調用者傳入的全部參數。也就是說,即便函數不定義任何參數,仍是能夠拿到參數的值:

 

 rest參數

 因爲JavaScript函數容許接收任意個參數,因而咱們就不得不用arguments來獲取全部參數:

function foo(a, b) {
    var i, rest = [];
    if (arguments.length > 2) {
        for (i=2; i < arguments.length; i++) {
            rest.push(arguments[i]);
        }
    }
    console.log('a = ' +a);
    console.log('b = ' +b);
    console.log(rest);
}

 

這種寫法,邏輯沒錯,可是略顯繁瑣,在 ES6 標準引入了 rest 參數,上面的參數能夠改寫爲:

function foo(a, b, ...rest) {
    console.log('a:', a);
    console.log('b:', b);
    console.log('rest:', rest)
}

 

注意 rest 在函數參數部分的寫法:(a, b ...rest)  這樣就能直接獲取除了 a, b 之外的全部參數。

當除了 a, b 參數之外,沒有 rest 參數,查看下返回結果:

 

測試-1:

用rest參數編寫一個sum()函數,接收任意個參數並返回它們的和。

function foo(...rest) {
    var j=0;
    for (var i of rest) {
        // console.log(i);
        j += i;
    }
    return j;
}

 

測試-2:

定義一個計算圓面積的函數area_of_circle(),它有兩個參數:

  • r: 表示圓的半徑;
  • pi: 表示π的值,若是不傳,則默認3.14
function area_of_circle(r, pi) {
    return pi?pi:3.14 * r **2;
    // if (pi === undefined) {
    //     pi = 3.14;
    // }
    // return pi * r**2;
}

 

 

變量做用域和解構賦值

若是一個變量在函數體內部申明,則該變量的做用域爲整個函數體,在函數體外不可引用該變量:

 

在來看一個函數內部的嵌套函數,判斷變量的使用:

這說明JavaScript的函數在查找變量時從自身函數定義開始,從「內」向「外」查找。若是內部函數定義了與外部函數重名的變量,則內部函數的變量將「屏蔽」外部函數的變量。

 

變量提高

JavaScript的函數定義有個特色,它會先掃描整個函數體的語句,把全部申明的變量「提高」到函數頂部:

 

在 JavaScript 中, 函數調用的時候須要作兩步:

  1. 分析(AO對象)

    (1) 先分析有沒有參數

    (2) 查看有沒有局部變量

    (3) 查看有沒有聲明函數

 

例 - 1:

 

例 - 2

(1)首先進行詞法分析:

(2)執行函數

執行函數時,不會對內部子函數進行賦值,直接跳過。

因爲JavaScript的這一怪異的「特性」,咱們在函數內部定義變量時,請嚴格遵照「在函數內部首先申明全部變量」這一規則。最多見的作法是用一個var申明函數內部用到的全部變量:

function foo() {
    var
        x = 1, // x初始化爲1
        y = x + 1, // y初始化爲2
        z, i; // z和i爲undefined
    // 其餘語句:
    for (i=0; i<100; i++) {
        ...
    }
}

 

全局做用域

不在任何函數內定義的變量就具備全局做用域。實際上,JavaScript默認有一個全局對象window,全局做用域的變量實際上被綁定到window的一個屬性:

 

 所以,直接訪問全局變量course和訪問window.course是徹底同樣的。

 

名字空間

全局變量會綁定到window上,不一樣的JavaScript文件若是使用了相同的全局變量,或者定義了相同名字的頂層函數,都會形成命名衝突,而且很難被發現。

減小衝突的一個方法是把本身的全部變量和函數所有綁定到一個全局變量中。例如:

// 惟一的全局變量MYAPP:
var MYAPP = {};

// 其餘變量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 其餘函數:
MYAPP.foo = function () {
    return 'foo';
};

 把本身的代碼所有放入惟一的名字空間MYAPP中,會大大減小全局變量衝突的可能。

 

局部做用域

 爲了解決塊級做用域,ES6中引入了新的關鍵字 let,用 let 代替 var 能夠申明一個塊級做用域的變量:

 

解構賦值

從ES6開始,JavaScript引入瞭解構賦值,能夠同時對一組變量進行賦值。

什麼是解構賦值?咱們先看看傳統的作法,如何把一個數組的元素分別賦值給幾個變量:

var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];

 如今,在ES6中,可使用解構賦值,直接對多個變量同時賦值:

若是數組自己還有嵌套,也能夠經過下面的形式進行解構賦值,注意嵌套層次和位置要保持一致:

解構賦值還能夠忽略某些元素:

 

若是須要從一個對象中取出若干屬性,也可使用解構賦值,便於快速獲取對象的指定屬性:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};
var {name, age, passport} = person;

console.log(name);  // 小明
console.log(age);   // 20
console.log(passport);  // G-12345678

 

對一個對象進行解構賦值時,一樣能夠直接對嵌套的對象屬性進行賦值,只要保證對應的層次是一致的:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school',
    address: {
        city: 'Beijing',
        street: 'No.1 Road',
        zipcode: '100001'
    }
};

var {name, address:{city, zip}} = person;

console.log(name); // '小明'
console.log(city); // 'Beijing'
console.log(zip); // undefined, 由於屬性名是zipcode而不是zip
// 注意: address不是變量,而是爲了讓city和zip得到嵌套的address對象的屬性:
console.log(address); // Uncaught ReferenceError: address is not defined

 

使用解構賦值對對象屬性進行賦值時,若是對應的屬性不存在,變量將被賦值爲undefined,這和引用一個不存在的屬性得到undefined是一致的。若是要使用的變量名和屬性名不一致,能夠用下面的語法獲取:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};

let {name, passport:id} = person;

console.log(name);  // 小明
console.log(id);    // G-12345678
// 注意: passport不是變量,而是爲了讓變量id得到passport屬性:
console.log(passport);  // Uncaught ReferenceError: passport is not defined

 

解構賦值還可使用默認值,這樣就避免了不存在的屬性返回undefined的問題:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678'
};

// 若是person對象沒有single屬性,默認賦值爲true:
var {name, single=true} = person;
name; // '小明'
single; // true

 

有些時候,若是變量已經被聲明瞭,再次賦值的時候,正確的寫法也會報語法錯誤:

// 聲明變量:
var x, y;
// 解構賦值:
{x, y} = { name: '小明', x: 100, y: 200};
// 語法錯誤: Uncaught SyntaxError: Unexpected token =

這是由於JavaScript引擎把{開頭的語句看成了塊處理,因而=再也不合法。解決方法是用小括號括起來:

({x, y} = { name: '小明', x: 100, y: 200});

 

解構賦值使用場景

解構賦值在不少時候能夠大大簡化代碼。例如,交換兩個變量xy的值,能夠這麼寫,再也不須要臨時變量:

var x=1, y=2;
[x, y] = [y, x]

快速獲取當前頁面的域名和路徑:

 

 使用解構賦值能夠減小代碼量,可是,須要在支持ES6解構賦值特性的現代瀏覽器中才能正常運行。目前支持解構賦值的瀏覽器包括Chrome,Firefox,Edge等。

 

2. 方法

在一個對象中綁定函數,稱爲這個對象的方法。

在 javascript 中,對象的定義是這樣的:

var xiaoming = {
    name: '小明',
    birth: 1990
};

能夠給對象 xiaoming 綁定一個函數,好比寫一個 age 方法,返回 xiaoming 的年齡:

var xiaoming = {
    name: '小明',
    brith: 1990,
    age: function () {
        var y = new Date().getFullYear();
        return y - this.brith
    }
};

console.log(xiaoming.age());    // 29

 綁定到對象上的函數稱爲方法,和普通函數也沒啥區別,這裏使用了關鍵字:this

在一個方法內部,this是一個特殊變量,它始終指向當前對象,也就是xiaoming這個變量。因此,this.birth能夠拿到xiaomingbirth屬性。

若是分開來寫,this就失效了。

function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

console.log(xiaoming.age());    // 29
console.log(getAge());  // NaN

 

注意這裏 this的使用:

若是以對象的方法形式調用,好比 xiaoming.age(),該函數的 this 指向被調用的對象,也就是 xiaoming,這是符合咱們預期的;

若是單獨調用函數,好比 getAge(),此時,該函數的 this 指向全局對象,也就是 window

var fn = xiaoming.age; // 先拿到xiaoming的age函數
fn(); // NaN

這樣是也不行的!要保證 this 指向正確,必須用 obj.xxx() 的形式調用!

 

apply

 要指定函數的this指向哪一個對象,能夠用函數自己的apply方法,它接收兩個參數,第一個參數就是須要綁定的this變量,第二個參數是Array,表示函數自己的參數。

apply修復getAge()調用:

function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

console.log(xiaoming.age()); // 29
console.log(getAge.apply(xiaoming, [])); // 29  this指向xiaoming, 函數參數爲空

 

另外一個與 apply() 相似的方法是 call() ,惟一的區別是:

  apply() 把參數打包成 Array 再傳入;

  call() 把參數按順序傳入.

好比調用Math.max(3, 5, 4),分別用apply()call()實現以下:

Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5

 對普通函數調用,咱們一般把this綁定爲null

 

3. 高階函數

JavaScript的函數其實都指向某個變量。既然變量能夠指向函數,函數的參數能接收變量,那麼一個函數就能夠接收另外一個函數做爲參數,這種函數就稱之爲高階函數。

一個最簡單的高階函數:

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

 當咱們調用add(-5, 6, Math.abs)時,參數xyf分別接收-56和函數Math.abs,根據函數定義,咱們能夠推導計算過程爲:

x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;

 

3.1 高階函數:map / reduce

(1)map()

map() 方法定義在 javascript 的 Array中,咱們調用 Array的Map() 方法,傳入本身的函數,就獲得一個新的 Array做爲結果:

 

map() 做爲高階函數,事實上它把運算規則抽象了,所以,可使用map來作比較複雜的操做,好比把 Array的全部數字轉爲字符串:

 

(2)reduce()

Array 的 reduce() 把一個函數做用在這個 Array 的,這個函數必須接收兩個參數,reduce() 把結果繼續和序列的下一個元素作累積計算,其效果就是:

[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)

例 -1  對一個 Array求和:

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

console.log([1, 2, 3, 4, 5, 6].reduce(sum)); // 21

例 - 2 對一個 Array 求積:

function product(arr) {
    return arr.reduce(function (x, y) {
        return x *y;
    })
}

console.log(product([1,2,3,4,5])); // 結果 120

例 - 3 把[1, 3, 5, 7, 9]變換成整數13579

var arr = [1, 3, 5, 7, 9].reduce(function (x, y) { return x*10+y });
console.log(arr); // 結果: 13579

例 - 4 不要使用JavaScript內置的parseInt()函數,利用map和reduce操做實現一個string2int()函數:

function string2int(s) {
    return s.split('').map(function (x) {
        return +x;
    }).reduce(function (x, y) {
        return x*10+y;
    })
}

console.log(string2int('13579'));   // 結果: 13579

例 - 5 請把用戶輸入的不規範的英文名字,變爲首字母大寫,其餘小寫的規範名字。輸入:['adam', 'LISA', 'barT'],輸出:['Adam', 'Lisa', 'Bart']

function normalize(arr) {
    return arr.map(function (x) {
        return x[0].toUpperCase() + x.slice(1).toLowerCase();
    })
}

console.log(normalize(['adam', 'LISA', 'barT'])); // ["Adam", "Lisa", "Bart"]

例 - 6 Array 字符串 --> 數字   Array 數字 --> 字符串

// Array 元素字符串 轉 數字
var arr = ['1', '2', '3'];
var r;
r = arr.map(Number);
console.log(r); // [1, 2, 3]


// Array 元素 數字 轉 字符串
s = r.map(String);
console.log(s); // ["1", "2", "3"]

 

3.2 高階函數: filter

filter也是一個經常使用的操做,它用於把Array的某些元素過濾掉,而後返回剩下的元素。

map()相似,Arrayfilter()也接收一個函數。和map()不一樣的是,filter()把傳入的函數依次做用於每一個元素,而後根據返回值是true仍是false決定保留仍是丟棄該元素。

例如,在一個 Array 中,刪掉偶數,只保留奇數,能夠這麼寫:

var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
    return x % 2 !== 0  // 過濾條件,條件爲true保留,爲false剔除
});

console.log(r); // [1, 5, 9, 15]

把一個Array中的空字符串刪掉,能夠這麼寫:

var arr = ['A', '', 'B', null, undefined, 'C', '  '];
var r = arr.filter(function (s) {
    return s && s.trim(); // 注意:IE9如下的版本沒有trim()方法
});
r; // ['A', 'B', 'C']

可見用filter()這個高階函數,關鍵在於正確實現一個「篩選」函數。

利用 filter,能夠去除 Array 的重複元素:

var
    r,
    arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];

r = arr.filter(function (element, index, self) {
    return self.indexOf(element) === index;
});

console.log(r); // ["apple", "strawberry", "banana", "pear", "orange"]

去除重複元素依靠的是indexOf老是返回第一個元素的位置,後續的重複元素位置與indexOf返回的位置不相等,所以被filter濾掉了。

例 - 1 請嘗試用filter()篩選出素數:

function get_primes(arr) {
    return arr.filter(
        x => {
            let result = true;
            let end =Math.sqrt(x);  // 開平方開出來是整數的要剔除掉
            let flag = 0;
            for (let i = 2; i <= end; i++) {    // 能被大於2且小於自己的整數整除的不是素數
                if (x % i === 0) {
                    flag = 1;
                    break;
                }
            }
            if (x === 1) {
                result = false;
            } else if ( flag === 0) {
                result = true;
            } else {
                result = false;
            }
            return result;
        })}

var
    x,
    r,
    arr = [];
for (x = 1; x < 100; x++) {
    arr.push(x);
}
r = get_primes(arr);
if (r.toString() === [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97].toString()) {
    console.log('測試經過!');
} else {
    console.log('測試失敗: ' + r.toString());
}

 

3.3 高階函數:sort()

JavaScript的Arraysort()方法就是用於排序的,可是排序結果可能讓你大吃一驚:

// 看上去正常的結果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];

// apple排在了最後:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']

// 沒法理解的結果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]

第二個排序把apple排在了最後,是由於字符串根據ASCII碼進行排序,而小寫字母a的ASCII碼在大寫字母以後。

第三個排序結果是什麼鬼?簡單的數字排序都能錯?

這是由於Arraysort()方法默認把全部元素先轉換爲String再排序,結果'10'排在了'2'的前面,由於字符'1'比字符'2'的ASCII碼小。

sort()方法也是一個高階函數,它還能夠接收一個比較函數來實現自定義的排序。

 

比較數字類型:

 

要按數字大小排序,咱們能夠這麼寫:

var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
    if (x> y) {
        return 1;
    }
    if (x< y) {
        return -1;
    }
    return 0;
});

console.log(arr);   // 結果: [1, 2, 10, 20]

從大到小排序:

var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
    if (x> y) {
        return -1;
    }
    if (x< y) {
        return 1;
    }
    return 0;
});

console.log(arr);   // 結果: [20, 10, 2, 1]

 

比較字母類型:

var arr = ['Google', 'apple', 'Microsoft'];

arr.sort(function (s1, s2) {
    x1 = s1.toLowerCase();
    x2 = s2.toLowerCase();
    if (x1 < x2) {
        return -1;
    }
    if (x1 > x2) {
        return 1;
    }
    return 0;
});

console.log(arr);   // 結果:["apple", "Google", "Microsoft"]

 

注意:sort() 方法會直接對 Array 進行修改,它返回的結果仍然是當前 Array

 

Array 其餘高階函數介紹

對於數組,除了map()reducefilter()sort()這些方法能夠傳入一個函數外,Array對象還提供了不少很是實用的高階函數。

every

every()方法能夠判斷數組的全部元素是否知足測試條件。

例如,給定一個包含若干字符串的數組,判斷全部字符串是否知足指定的測試條件:

var arr = ['Apple', 'pear', 'orange'];
console.log(arr.every(function (s) {
    return s.length > 0;    // 由於每一個元素長度都大於0,因此返回 true
}));

console.log(arr.every(function (s) {
    return s.toLowerCase() === s;   // 原元素不是每一個都是小寫,因此返回false
}));

 

注意:every 高階函數,針對 Array 中每一個元素進行檢查,若是有一個不知足條件則返回 false。

 

find

find()方法用於查找符合條件的第一個元素,若是找到了,返回這個元素,不然,返回undefined

var arr = ['Apple', 'pear', 'orange'];
console.log(arr.find(function (s) {
    return s.toLowerCase() === s;   // pear 返回符合條件的第一個元素
}));

console.log(arr.find(function (s) {
    return s.toUpperCase() === s;   // undefined 由於沒有任何一個元素知足條件,則返回 undefined
}));

 

findIndex

findIndex()find()相似,也是查找符合條件的第一個元素,不一樣之處在於findIndex()會返回這個元素的索引,若是沒有找到,返回-1

var arr = ['Apple', 'pear', 'orange'];

console.log(arr.findIndex(function (s) {
    return s.toLowerCase() === s;   // 返回第一個符合條件元素的索引,沒有則返回 -1
}));

console.log(arr.findIndex(function (s) {
    return s.toUpperCase() === s;   // 返回第一個符合條件元素的索引,沒有則返回 -1
}));

 

3.4 閉包

函數做爲返回值

高階函數除了能夠接受函數做爲參數外,還能夠把函數做爲結果值返回。

function lazy_sum(arr) {
    var sum = function () {
        return arr.reduce(function (x, y) {
            return x+y;
        })
    };
    return sum;
}

var f = lazy_sum([1,2,3,4,5]);
console.log(f); // 返回 function () { ... }
console.log(f());   // 返回結果: 15

在這個例子中,咱們在函數lazy_sum中又定義了函數sum,而且,內部函數sum能夠引用外部函數lazy_sum的參數和局部變量,當lazy_sum返回函數sum時,相關參數和變量都保存在返回的函數中,這種稱爲「閉包(Closure)」的程序結構擁有極大的威力。

請再注意一點,當咱們調用lazy_sum()時,每次調用都會返回一個新的函數,即便傳入相同的參數:

var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
f1 === f2; // false

 f1()f2()的調用結果互不影響。

 

閉包

返回閉包時牢記的一點就是:返回函數不要引用任何循環變量,或者後續會發生變化的變量

 

3.5 箭頭函數

ES6標準新增了一種新的函數:Arrow Function(箭頭函數)。

具體寫法:

var fn = x => x * x;
console.log(fn(10));    // 100

x => x * x;
等價於
function (x) { return x * x}

若是參數不是一個,就須要用括號()括起來:

// 兩個參數:
(x, y) => x * x + y * y

// 無參數:
() => 3.14

// 可變參數:
(x, y, ...rest) => {
    var i, sum = x + y;
    for (i=0; i<rest.length; i++) {
        sum += rest[i];
    }
    return sum;
}

 

練習 1:

請使用箭頭函數簡化排序時傳入的函數:

var arr = [10, 20, 1, 2];
arr.sort((x, y) => {
    return (x>y) ? 1:-1; // 在作 if 判斷時,首先考慮可否使用 三元運算
});
console.log(arr); // [1, 2, 10, 20]

 

 

附加一道面試題:

  arr = [1, 2, 2, 3, 4, 5, 4, 5] 去重並改變原有順序

 

方法1:

經過索引判斷

arr = [1, 2, 2, 3, 4, 5, 4, 5];


function uniq(arr) {
    var temp = [];
    for (var i=0; i<arr.length; i++) {
        if (temp.indexOf(arr[i]) === -1) {  // 當值存在則返回索引位置,不存在則返回 -1
            temp.push(arr[i])   // 當元素不存在則添加元素
        }
    }
    return temp;
}

console.log(uniq(arr));

 

方法2:

經過 Set 類型直接去重

arr = [1, 2, 2, 3, 4, 5, 4, 5];

console.log([... new Set(arr)]);    // '...' 是 js中的擴展運算符, 這裏是將 set 類型遍歷轉換爲 數組類型
相關文章
相關標籤/搜索