JavaScript 解密 —— 函数初步

一、First-class objects

在理解函数作为一等对象前,先列举下 JavaScript 中对象支持的操作:

  • 可以通过 {} 字面量创建
  • 可以被赋值给变量、数组项,可以作为其他对象的属性

    1
    2
    3
    var ninja = {}  // 赋值给变量
    ninjaArray.push({}) // 作为数组项
    ninja.data = {} // 作为其他对象的属性
  • 可以作为函数的参数或返回值

  • 可以拥有支持动态创建与赋值的属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function hide(ninja) {
    ninja.visibility = false
    }
    hide({}) // 对象作为函数参数

    function returnNewNinja() {
    return {} // 对象作为函数返回值
    }

    var ninja = {}
    ninja.name = "Hanzo" // 动态创建的属性
函数作为一等对象

JavaScript 中的函数拥有作为对象的所有特性,因此可以像对待任何其他对象一样对其进行使用。

  • 通过字面量创建: function ninjaFunction() {}
  • 将函数赋值给变量:var ninjaFunction = function() {}
  • 数组中加入函数作为数据项:ninjaArray.push(function() {})
  • 函数作为对象的属性:ninja.data = function() {}
  • 函数作为其他函数的参数或返回值

    1
    2
    3
    4
    5
    6
    7
    8
    function call(ninjaFunction) {
    ninjaFunction()
    }
    call(function() {}) // 函数作为参数

    function returnNewNinjaFunction() {
    return function() {} // 函数作为返回值
    }
  • 函数可以拥有支持动态创建和赋值的属性

    1
    2
    var ninjaFunction = function () {}
    ninjaFunction.name = "Hanzo"

函数即对象,只不过拥有一项额外的特性(能够被调用)以完成一些特定的动作。任何可以对对象做出的操作,同样可以应用在函数身上。

Callback functions

回调函数即在后续的某个指定的时间节点被其他代码调用(call back)的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var text = "Domo arigato"

function useless(ninjaCallback) {
console.log("In useless function")
return ninjaCallback()
}

function getText() {
console.log("In getText function")
return text
}

console.log(useless(getText))
// In useless function
// In getText function
// Domo arigato

或者

1
2
3
4
5
6
7
8
var text = 'Domo arigato'

function useless(ninjaCallback) {
return ninjaCallback()
}

console.log(useless(function() { return text }))
// Domo arigato

回调函数在数组排序中的使用:

1
2
3
4
var values = [0, 3, 2, 5, 7, 4, 8, 1]
values.sort(function(value1, value2){
return value1 - value2
})

Self-memoizing functions

memoization 是指构建一个特殊的函数,该函数可以将之前计算过的值缓存在自己内部,之后再做同样的计算时则可以直接读取缓存的值而不必重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function isPrime(value) {
if (!isPrime.answers) {
isPrime.answers = {}
}
if (isPrime.answers[value] !== undefined) {
return isPrime.answers[value]
}

var prime = value !== 1
for (var i = 2; i < value; i++) {
if (value % i === 0) {
prime = false
break
}
}
return isPrime.answers[value] = prime
}

console.log(isPrime(5))
console.log(isPrime.answers)
// true
// { '5': true }

二、函数定义

JavaScript 提供以下几种定义函数的方式:

  • 函数声明(表达式):function myFun() { return 1 }
  • Arrow function:myArg => myArg * 2
  • 函数构造器:new Function('a', 'b', 'return a + b')
  • 生成器函数:function* myGen() { yield 1 }

函数声明是最基础的定义函数的方式,其基本格式如下:

1
2
3
4
function myFunctionName(myFirstArg, mySecondArg) {
myStatement1
myStatement2
}

函数声明代码可以出现在另一个函数内部:

1
2
3
4
5
6
function ninja() {
function hiddenNinja() {
return "ninja here"
}
return hiddenNinja()
}

函数表达式

作为 JavaScript 中的一等对象,函数可以通过字面量创建,可以赋值给变量和对象属性,可以作为另一个函数的参数或返回值。
也因此可以将其作为表达式使用,即成为其他代码语句的一部分(比如放在赋值语句的等号右边、充当参数或返回值等)

1
2
3
4
5
var myFunc = function() {}

myFunc(function() { // 作为参数
return function() {} // 作为返回值
})

函数表达式甚至可以放置在通常应该使用函数标识符的地方,在声明的同时立即完成调用,称为 immediate function

1
2
3
4
5
6
7
myFunctionName(3)  // 普通调用
(function() {})(3) // immediate call

+function () {} ()
-function () {} ()
!function () {} ()
~function () {} ()

Arrow function

在很多情况下,arrow function 可以看作对普通函数表达式的简化。如之前的排序示例:

1
2
3
4
var values = [0, 3, 2, 5, 7, 4, 8, 1]
values.sort(function(value1, value2){
return value1 - value2
})

使用 arrow function 则可以改为如下形式:

1
2
var values = [0, 3, 2, 5, 7, 4, 8, 1]
values.sort((value1, value2) => value1 - value2)

arrow function 最简单的语法形式为 param => expression,一个基本示例如下:

1
2
var greet = name => "Greetings, " + name
console.log(greet('Oishi')) // Greetings, Oishi

更复杂一点的形式如:

1
2
3
4
5
6
var greet = name => {
var helloString = 'Greetings, '
return helloString + name
}

console.log(greet('Oishi')) // Greetings, Oishi

参数

Rest prarmeters

1
2
3
4
5
6
7
8
function multiMax(first, ...remainingNumbers){
var sorted = remainingNumbers.sort(function(a, b){
return b - a
})
return first * sorted[0]
}

console.log(multiMax(3, 1, 2, 3)) // 9

Default parameters

1
2
3
4
5
6
function performAction(ninja, action = "skulking") {
return ninja + " " + action
}

console.log(performAction("Fuma")) // Fuma skulking
console.log(performAction("Yagyu", "sneaking")) // Yagyu sneaking

甚至可以这样写:

1
2
3
4
5
function performAction(ninja, action = "skulking",
message = ninja + " " + action) {
return message
}
console.log(performAction("Yoshi")) // Yoshi skulking

三、函数调用

作为“函数”调用

1
2
3
4
5
6
7
function ninja(name) { console.log(name) }
ninja('Hattori') // Hattori

var samurai = function(name) { console.log(name) }
samurai('Hattori'); // Hattori

(function(name) { console.log(name) })('Hattori') // Hattori

作为方法调用:

1
2
3
var ninja = {}
ninja.skulk = function() {}
ninja.skulk()

作为方法调用与作为函数调用的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function whatsMyContext() {
return this
}
console.log(whatsMyContext() === global) // true

var getMyThis = whatsMyContext
console.log(getMyThis() === global) // true

var ninja1 = {
getMyThis: whatsMyContext
}
console.log(ninja1.getMyThis() === ninja1) // true

var ninja2 = {
getMyThis: whatsMyContext
}
console.log(ninja2.getMyThis() === ninja2) // true

作为构造器函数:

1
2
function whatsMyContext(){ return this }
new whatsMyContext()

注意与函数构造器(如 new Function('a', 'b', 'return a + b') )的区别:函数构造器用来从字符串中动态地创建函数,而构造器函数则用来创建对象实例。

构造器函数在调用时一般会执行以下操作:

  • 创建一个空的对象
  • 新创建的对象绑定给 this 参数传递给构造器,成为构造器函数的上下文
  • 新创建的对象作为 new 操作的返回值被返回
1
2
3
4
5
6
7
8
9
10
11
function Ninja() {
this.skulk = function() {
return this
}
}

var ninja1 = new Ninja()
console.log(ninja1.skulk() === ninja1) // true

var ninja2 = new Ninja()
console.log(ninja2.skulk() === ninja2) // true

若构造器函数的定义中本身具有返回值,则分为两种情况:

  • 若构造器的定义中返回了一个非对象值(字符串、数字等),则该返回值被忽略,由构造器创建的 this 对象被返回作为 new 表达式的值
  • 若构造器的定义中返回了一个对象,则该对象作为 new 表达式的值,由构造器创建的 this 对象则被忽略
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Ninja() {
    this.skulk = function() {
    return true
    }
    return 1
    }

    var ninja = new Ninja()
    console.log(ninja) // Ninja { skulk: [Function] }
    console.log(ninja.skulk()) // true
1
2
3
4
5
6
7
8
9
10
11
12
var puppet = {
rules: false
}

function Emperor() {
this.rules = true
return puppet
}

var emperor = new Emperor()
console.log(emperor) // { rules: false }
console.log(emperor.rules) // false

applycall 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function juggle() {
var result = 0
for (var n = 0; n < arguments.length; n++) {
result += arguments[n]
}
this.result = result
}

var ninja1 = {}
var ninja2 = {}

juggle.apply(ninja1, [1,2,3,4])
juggle.call(ninja2, 5,6,7,8)

console.log(ninja1.result) // 10
console.log(ninja2.result) // 26

参考资料

Secrets of the JavaScript Ninja, Second Edition