函数是“一等对象”
函数式编程的风格是将函数组合起来,而不是像命令式语言那样指定一系列步骤;理解 JavaScript 中包含的函数式语言的特性至关重要。
JavaScript 中的函数是一等对象(first-class objects, first-class citizens),函数作为一种值,可以和任何其他类型的值一样被使用。
JavaScript 中对象的特性:
- 可以通过字面量
{}
创建; - 可以赋值给变量、数组元素、对象的属性;
- 可以作为参数传递给函数(回调函数);
- 可以作为函数的返回值返回;
- 拥有属性,可以动态创建属性。
JavaScript 的函数也是对象,同样拥有上述特性,同时具有可以被调用的特殊属性。
|
|
任何可以出现表达式的地方都可以创建函数。
这样的写法紧凑、易懂(函数的定义的就在调用函数的地方)。
若函数不会在多处被引用,也可以避免定义一个全局函数名污染全局命名空间。
1 2 3 4 5 6 7 8 9
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 表示两个值相等。
|
|
实现一个存储回调函数的集合:
|
|
实现一个具有“记忆”功能的函数,避免一些重复的耗时计算:
|
|
这样写也存在一些问题:
- 缓存是以空间换性能,需根据实际需求进行选择。
- 缓存和函数的逻辑混杂在一起,函数应该做到职责单一。
- 函数的性能会受到之前输入参数的影响,导致不易测试。
定义函数
定义函数的方法可以分为四类:
- 函数声明和函数表达式;
- 箭头函数(也叫 lambda functions);
- 函数构造器;
- 动态创建、解析代码时有应用。
- Generator 函数。
函数定义和函数表达式
函数声明必须作为一个独立的语句出现,它可以出现在另一个函数里(函数式特点)或代码块中。
函数必须要有函数名,否则无法被调用(引用不到)。
函数表达式总是作为一个语句的一部分出现;函数名可选。
调用函数的 ()
前面可以是任何能解析成一个函数的表达式,所以有了 IIFE(immediately invoked function expression)的写法。
IIFE 中的函数表达式要包在括号里,指示 JavaScript 解析器将它作为表达式而不是语句解析(一元操作符也可以起到这种效果)。
- 不加括号的话,以
function
开头的代码会被作为函数声明来解析,而函数声明必须有函数名,会报错。
- 不加括号的话,以
IIFE 可以用来模拟模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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)的语法 ...
。
剩余参数是一个数组,只能作为最后一个参数,否则会报语法错误。
1 2 3 4 5
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 引入默认参数语法。
由于参数的解析顺序为从左往右,写在后面的默认参数可以引用写在前面的参数。
1 2 3 4 5 6 7 8 9 10 11 12
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
中对应“元素”的值。
1 2 3 4 5 6 7
(function foo(a) { var save = a arguments[0] = "bar" console.log(a) // bar a = save console.log(arguments[0]) // foo })("foo")
- 严格模式下,
arguments
就不是函数参数的别名。
1 2 3 4 5 6 7 8
"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
是 undefined
。
2. 作为对象的方法被调用
函数作为对象的方法被调用时,this
是“拥有”该方法的对象。
|
|
两个对象需要共用相同的逻辑时,不需要各自定义方法,只需要指向同一个函数,函数作为不同对象的方法被调用时,就能通过 this
访问到方法所属的对象。
3. 作为构造函数被调用
构建函数是用来创建和初始化对象实例的函数(不应被用作其它用途)。
- 若函数作为构造函数使用,调用时要加上
new
关键字,除此之外构造函数和普通函数并无差别。 - 构造函数不是函数构造器。
- 函数构造器可以用字符串构建函数(
new Function("a", "b", "return a + b")
)。
- 函数构造器可以用字符串构建函数(
调用构造函数时发生的动作:
创建一个空对象;
创建的空对象被传入构造函数,并作为构造函数的
this
;新创建的对象作为返回值被返回(构造函数显式返回其它值时是例外)。
1 2 3 4 5 6 7 8 9
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
表达式的值,新创建的对象被丢弃;若显式返回的不是对象,则该值被丢弃,新创建的对象照常返回。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
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
访问传入参数的方式相同。
|
|
简易版 forEach
的实现:
|
|
捋顺函数上下文
箭头函数没有自己的函数上下文,它的 this
值是函数定义时“记住”的那个包含箭头函数定义的外层环境的 this
。
若箭头函数定义在构造函数里,它的
this
是新创建的对象。若箭头函数定义在全局的对象字面量里,它的
this
是window
对象。1 2 3
const foo = () => console.log(this === window) foo() // true (() => console.log(this === window))() // true
函数的 bind
方法会返回一个新函数(不会修改原函数),新函数的函数体和原函数的相同,但新函数的上下文 this
永远指向 bind
的第一个参数指定的对象,无论新函数如何被调用,this
都不会变。
|
|