Function in JavaScript (Part 1)
Contents
函数是“一等对象”
函数式编程的风格是将函数组合起来,而不是像命令式语言那样指定一系列步骤;理解 JavaScript 中包含的函数式语言的特性至关重要。
JavaScript 中的函数是一等对象(first-class objects, first-class citizens),函数作为一种值,可以和任何其他类型的值一样被使用。
JavaScript 中对象的特性:
- 可以通过字面量
{}
创建; - 可以赋值给变量、数组元素、对象的属性;
- 可以作为参数传递给函数(回调函数);
- 可以作为函数的返回值返回;
- 拥有属性,可以动态创建属性。
JavaScript 的函数也是对象,同样拥有上述特性,同时具有可以被调用的特殊属性。
function foo() {}
var f = function() {}
arr.push(function() {})
obj.data = function() {}
function call(foo) { foo() }
call(function() {})
function returnFunc() { return function() {} }
var func = function() {}
func.name = "foo"
任何可以出现表达式的地方都可以创建函数。
-
这样的写法紧凑、易懂(函数的定义的就在调用函数的地方)。
-
若函数不会在多处被引用,也可以避免定义一个全局函数名污染全局命名空间。
function foo(cb) { return cb() } console.log(foo(function() { return "synchronous callback" })) /* function cb() { return "synchronous callback2" } console.log(foo(cb)) // 传入函数名 */ console.log("end") document.body.addEventListener("click", function() { console.log("asynchronous callback invoked by the browser") })
数组排序的函数式写法中,我们不把决定顺序先后的逻辑交由排序算法(这部分逻辑在每个场景下都是不同的),而是提供一个执行比较操作的回调函数,供排序算法调用;回调函数返回正数表示比较的两个值需要交换顺序,返回负数表示不需要交换,返回 0 表示两个值相等。
var arr = [0, 3, 2, 5, 7, 4, 8, 1]
arr.sort(function(v1, v2) { return v1 - v2 }) // 升序
console.log(arr)
实现一个存储回调函数的集合:
var store = {
nextId: 1,
cache: {},
add: function(fn) {
if (!fn.id) { // *
fn.id = this.nextId++
this.cache[fn.id] = fn
return true
}
}
}
function foo() {}
store.add(foo)
实现一个具有“记忆”功能的函数,避免一些重复的耗时计算:
function isPrime(value) {
if (!isPrime.answers) {
isPrime.answers = {} // create the cache
}
if (isPrime.answers[value] !== undefined) {
return isPrime.answers[value]
}
var prime = value !== 1 // 1 不是质数
for (var i = 2; i < value; i++) { // ignore the inefficient algorithm
if (value % i === 0) {
prime = false
break
}
}
return isPrime.answers[value] = prime
}
console.log(isPrime(5))
这样写也存在一些问题:
- 缓存是以空间换性能,需根据实际需求进行选择。
- 缓存和函数的逻辑混杂在一起,函数应该做到职责单一。
- 函数的性能会受到之前输入参数的影响,导致不易测试。
定义函数
定义函数的方法可以分为四类:
- 函数声明和函数表达式;
- 箭头函数(也叫 lambda functions);
-
函数构造器
;
- 动态创建、解析代码时有应用。
- Generator 函数。
函数定义和函数表达式
函数声明必须作为一个独立的语句出现,它可以出现在另一个函数里(函数式特点)或代码块中。
函数必须要有函数名,否则无法被调用(引用不到)。
函数表达式总是作为一个语句的一部分出现;函数名可选。
调用函数的 ()
前面可以是任何能解析成一个函数的表达式,所以有了 IIFE(immediately invoked function expression)的写法。
-
IIFE 中的函数表达式要包在括号里,指示 JavaScript 解析器将它作为表达式而不是语句解析(一元操作符也可以起到这种效果)。
- 不加括号的话,以
function
开头的代码会被作为函数声明来解析,而函数声明必须有函数名,会报错。
- 不加括号的话,以
-
IIFE 可以用来模拟模块。
function foo() { function innerFoo() {} } var myFunc = function() {} // * function() {} 是函数表达式 (function namedFunctionExpression() {})() // 具名函数表达式 function() {console.log("hi")}() // Uncaught SyntaxError: Function statements require a function name function foo() {console.log("hi")}() // Uncaught SyntaxError: Unexpected token ')' +function(){}() -function(){}() !function(){}() ~function(){}()
箭头函数
箭头函数的标志是胖箭头(fat-arrow)操作符 =>
。
箭头函数语法:
- 只有一个参数时,参数可以不用括号包裹。
- 函数体只有一个表达式时,返回值就是该表达式的值,可以显式写
return
语句。 - 函数体是代码块时,
return
语句和普通函数的写法相同;省略return
语句则返回undefined
。
实参和形参
参数(parameter,形参)是函数定义中列出的变量。
实参(argument)是调用函数时传递过去的值。
实参会按传入的顺序和形参一一对应,两者数量不一致时不会报错,多出的参数(没有对应的实参传入)的值是 undefined
。
ES6 引入了剩余参数(rest parameter)的语法 ...
。
-
剩余参数是一个数组,只能作为最后一个参数,否则会报语法错误。
function multiMax(first, ...remaining) { var sorted = remaining.sort(function(v1, v2) { return v2 - v1 }) return first * sorted[0] } console.log(multiMax(3, 1, 2, 3)) // 9
JavaScript 不支持函数重载(函数名相同,函数签名不同)。
- 对于 JavaScript 而言,函数签名就是参数个数。
ES6 引入默认参数语法。
-
由于参数的解析顺序为从左往右,写在后面的默认参数可以引用写在前面的参数。
function foo1(a, b = "default") { return a + b } function foo2(a, b = "default", msg = a + b) { // 不建议这样写 return msg } // pre ES6 function foo3(a, b) { b = typeof action === "undefined" ? "default" : action return a + b }
隐式参数
函数体里可以访问到两个隐式传入(不在函数签名里)的参数:this
,arguments
。
this
指函数上下文(function context),表示函数是在哪个对象上被调用的。arguments
(对象)代表传入函数的所有实参,包括函数签名里没有指定形参的实参。-
arguments
的length
属性的值是实参的个数,里面的实参可以通过数组下标的形式访问。 -
arguments
结构和数组类似,但它不是数组,不能在arguments
身上使用数组方法(如sort
)。- 剩余参数是真数组。
-
arguments
是函数参数的别名,它们指向相同的地址。- 若更改
arguments
中“元素”的值,对应形参的值也会随之改变;反之,改变形参的值也会改变arguments
中对应“元素”的值。
(function foo(a) { var save = a arguments[0] = "bar" console.log(a) // bar a = save console.log(arguments[0]) // foo })("foo")
- 严格模式下,
arguments
就不是函数参数的别名。
"use strict"; (function foo(a) { var save = a arguments[0] = "bar" console.log(a) // foo a = save console.log(arguments[0]) // bar })("foo")
- 若更改
-
函数的调用
1. 作为函数被直接调用
非严格模式下(全局代码里)函数被直接调用时,它的 this
是全局上下文,即 window
对象。
// * 全局代码里的 this 就是 window。
// 可以理解成通过一个全局对象构造器创建了一个全局对象单例 window,this 自然指向 window,
// 全局函数的调用实际上是调用 window 对象的方法,所以它们的 this 都指向 window
console.log(this === window) // true
function foo() { console.log("foo") }
foo() // foo
// window “拥有”所有全局变量和全局函数
window.foo() // foo
严格模式下(全局代码里)函数被直接调用时,它的 this
是 undefined
。
2. 作为对象的方法被调用
函数作为对象的方法被调用时,this
是“拥有”该方法的对象。
function myContext() {
return this
}
console.log(myContext() === window) // true
var getMyContext = myContext // 创建对 myContext 函数的一个引用,并没有创建函数的新实例
console.log(getMyContext() === window) // true
var obj1 = {
getMyContext: myContext // getMyContext 属性的值是对 myContext 函数的一个引用,myContext 本身没有变成 obj1 的一个方法
}
console.log(obj1.getMyContext() === obj1) // true
var obj2 = {
getMyContext: myContext
}
console.log(obj2.getMyContext() === obj2) // true
两个对象需要共用相同的逻辑时,不需要各自定义方法,只需要指向同一个函数,函数作为不同对象的方法被调用时,就能通过 this
访问到方法所属的对象。
3. 作为构造函数被调用
构建函数是用来创建和初始化对象实例的函数(不应被用作其它用途)。
- 若函数作为构造函数使用,调用时要加上
new
关键字,除此之外构造函数和普通函数并无差别。 - 构造函数不是函数构造器。
- 函数构造器可以用字符串构建函数(
new Function("a", "b", "return a + b")
)。
- 函数构造器可以用字符串构建函数(
调用构造函数时发生的动作:
-
创建一个空对象;
-
创建的空对象被传入构造函数,并作为构造函数的
this
; -
新创建的对象作为返回值被返回(构造函数显式返回其它值时是例外)。
function Door() { this.context = function() { return this } } var door1 = new Door() var door2 = new Door() console.log(door1.context() === door1) // true console.log(door2.context() === door2) // true
-
若构造函数显式返回的是一个对象,则该对象会成为
new
表达式的值,新创建的对象被丢弃;若显式返回的不是对象,则该值被丢弃,新创建的对象照常返回。function Board() { this.context = function() { return true } return 1 } console.log(Board() === 1) // true const board = new Board() console.log(typeof board === "object") // true console.log(typeof board.context === "function") // true var s = { price: 1230 } function Stick() { this.price = 123 this.context = function() { return this } return s } var stick = new Stick() console.log(stick === s, stick.price) // true, 1230 console.log(stick.context()) // Uncaught TypeError: stick.context is not a function
-
构造函数的用途决定了它的内部代码实现和普通函数不同,带来的结果是将构造函数作为一般函数使用时基本无用,于是有了区分构造函数和普通函数的命名规范:
- 函数名和方法名通常以动词开头,描述行为;首字母小写。
- 构造函数名通常是描述对象的名词;首字母大写。
构造函数提供了创建遵循同一种模式的对象的模板。
4. 通过函数的 apply
或 call
方法被调用
函数的 apply
和 call
方法可以在调用函数时显式指定一个对象(第一个参数)作为函数上下文 this
。
apply
和 call
只有参数传递写法上的差别,函数体里通过 arguments
访问传入参数的方式相同。
function sum() {
var result = 0
for (var n = 0; n < arguments.length; n++) {
result += arguments[n]
}
this.result = result
}
var o1 = {}
var o2 = {}
sum.apply(o1, [1, 2, 3, 4])
sum.call(o2, 5, 6, 7, 8)
console.log(o1.result, o2.result) // 10 26
简易版 forEach
的实现:
function forEach(arr, cb) {
for (var n = 0; n < arr.length; n++) {
cb.call(arr[n], n)
}
}
var letters = [{ name: "a" }, { name: "b" }, {name: "c"}]
forEach(letters, function(index) {
console.log(this === letters[index]) // true true true
})
捋顺函数上下文
箭头函数没有自己的函数上下文,它的 this
值是函数定义时“记住”的那个包含箭头函数定义的外层环境的 this
。
-
若箭头函数定义在构造函数里,它的
this
是新创建的对象。 -
若箭头函数定义在全局的对象字面量里,它的
this
是window
对象。const foo = () => console.log(this === window) foo() // true (() => console.log(this === window))() // true
函数的 bind
方法会返回一个新函数(不会修改原函数),新函数的函数体和原函数的相同,但新函数的上下文 this
永远指向 bind
的第一个参数指定的对象,无论新函数如何被调用,this
都不会变。
<button id="test1">Click1</button>
<button id="test2">Click2</button>
<button id="test3">Click2</button>
<script>
function Button1() {
this.clicked = false
this.click = function() {
this.clicked = true
console.log("button1 clicked")
}
}
var button1 = new Button1()
var ele1 = document.querySelector("#test1")
// 事件处理器的回调函数的 this 指向注册且发生了该事件的 DOM 元素,
// 此处即 DOM 中的按钮,不是 button 对象
ele1.addEventListener("click", button1.click)
// clicked 状态没有设置在 button1 对象上(log after click)
// button1: {clicked: false, click: ƒ}
function Button2() {
this.clicked = false
this.click = () => {
this.clicked = true
console.log("button2 clicked")
}
}
var button2 = new Button2()
var ele2 = document.querySelector("#test2")
ele2.addEventListener("click", button2.click)
// log after click
// button2: {clicked: true, click: ƒ}
// bind 解法
// ele1.addEventListener("click", button.click.bind(button))
</script>