关注前端小讴,阅读更多原创技术文章
- 函数是对象,每个函数都是
Function
类型的实例,都与其他引用类型一样具有属性和方法 - 函数名是指向函数对象的指针,不会与某个函数绑定(一个函数可能会有多个名字)
4 种定义方式
// 1.函数声明定义
function sum(num1, num2) {
return num1 + num2
}
// 2.函数表达式定义
let sum = function (num1, num2) {
return num1 + num2
}
// 4.箭头函数定义
let sum = (num1, num2) => {
return num1 + num2
}
// 4.Function构造函数定义
/* 不推荐使用构造函数定义,因为会造成解析2次代码(1次解析常规js代码,1次解析传入构造函数中的字符串) */
var sum = new Function('num1', 'num2', 'return num1+num2')
10.1 箭头函数
- 任何可以使用函数表达式的地方都可以使用箭头函数
let arrowSum = (a, b) => {
return a + b
}
let functionExpressionSum = function (a, b) {
return a + b
}
console.log(arrowSum(5, 8)) // 13
console.log(functionExpressionSum(5, 8)) // 13
- 如果只有1 个参数,可以不用括号(多个参数或无参数必须用括号)
let double = (x) => {
return x * 3
}
console.log(double(3)) // 9
- 可以不用大括号,如果这样则箭头后面只能有 1 行代码(赋值操作或表达式),且隐式返回这行代码的值(不能有
return
)
let person = {}
let setName = (obj) => (obj.name = 'Matt') // 相当于 { return obj.name = 'Matt' }
// let setName = (obj) => { return (obj.name = 'Matt') } // 用大括号的写法
setName(person)
console.log(person.name) // 'Matt'
箭头函数不适用的场合:
- 不能使用
arguments
、super
和new.target
- 不能用作构造函数
- 没有
prototype
属性()
- 不能使用
10.2 函数名
- 函数名是指向函数的指针,一个函数可以有多个名称
function sum(num1, num2) {
return num1 + num2
}
console.log(sum(10, 10)) // 20
var anotherSum = sum // 使用不带括号的函数名是访问函数指针,而非调用函数
console.log(anotherSum(10, 10)) // 20
sum = null // 切断sum与函数的关系
console.log(anotherSum(10, 10)) // 20,anotherSum()仍可正常调用
// console.log(sum(10, 10)) // 会报错,sum is not a function
- ES6 所有函数对象都暴露一个只读的
name
属性,默认保存函数标识符(字符串化的函数名) - 无函数名则标识成空字符串,使用
Function
构造函数创建则标识成anonymous
function foo() {} // 函数声明
let bar = function () {} // 函数表达式
let baz = () => {} // 箭头函数
console.log(foo.name) // 'foo'
console.log(bar.name) // 'bar'
console.log(baz.name) // 'baz'
console.log((() => {}).name) // 空字符串
console.log(new Function().name) // 'anonymous'
- 如果函数是一个获取函数、设置函数或使用
bind()
实例化,标识符前会加上一个前缀
console.log(foo.bind(null).name) // 'bound foo' ,标识符前加前缀
let dog = {
years: 1,
get age() {
return this.years
},
set age(newAge) {
this.years = newAge
},
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age') // 获取属性描述符
console.log(propertyDescriptor) // { get: [Function: get age], set: [Function: set age], enumerable: true, configurable: true }
console.log(propertyDescriptor.get.name) // 'get age',标识符前加前缀
console.log(propertyDescriptor.set.name) // 'set age',标识符前加前缀
10.3 理解参数
- JS 函数不关心传入的参数个数和数据类型
- 可在非箭头函数内部访问
arguments
类数组对象,取得每个参数(如arguments[0]
) - JS 函数的命名参数在调用时不用必须匹配函数签名,不存在验证命名参数的机制
function sayHi(name, message) {
console.log(`Hello ${name}, ${message}`)
}
function sayHi2(name, message) {
console.log(`Hello ${arguments[0]}, ${arguments[1]}`)
}
function sayHi3() {
// 没有命名参数
console.log(`Hello ${arguments[0]}, ${arguments[1]}`)
}
sayHi('Jake', 'How are you') // 'Hello Jake, How are you'
sayHi2('Jake', 'How are you') // 'Hello Jake, How are you'
sayHi3('Jake', 'How are you') // 'Hello Jake, How are you'
- 可以通过
arguments.length
检查传入参数的个数
function howManyArgs() {
console.log(arguments.length) // 检查参数个数
}
howManyArgs('string', 45) // 2
howManyArgs() // 0
howManyArgs(12) // 1
arguments
对象可以跟命名参数一起使用
function doAdd(num1, num2) {
if (arguments.length === 1) {
console.log(num1 + 10)
} else if (arguments.length === 2) {
console.log(arguments[0] + num2)
}
}
doAdd(10) // 20,10+10
doAdd(30, 20) // 50,30+20
arguments
对象的值始终会与命名参数同步- 在内存中中分开(非访问同一个地址),仅值同步
- 调用函数时未传的参数,不会因为
arguments
改变而改变(始终是 undefined)
function args(num1, num2) {
arguments[1] = 10
console.log(num1, num2)
}
args(2, 3) // 2 10
function args2(num1, num2) {
arguments[1] = 10
console.log(num1, num2)
}
args2(2) // 2 undefined,调用函数时只传入一个参数,修改arguments不会改变第二个命名参数,仍旧是undefined
function args3(num1, num2) {
num1 = 10
console.log(arguments[0], arguments[1])
}
args3(2, 3) // 10 3
严格模式下,
arguments
会有一些变化:arguments
对象的值与命名参数不再同步,修改arguments
不再对命名参数产生影响- 在函数中重写
arguments
对象会报错,代码也不会执行
function strictArgs(num1, num2) {
'use strict'
arguments[1] = 10
console.log(num1, num2)
// arguments = [] // SyntaxError: Unexpected eval or arguments in strict mode,不能重写arguments
}
strictArgs(2, 3) // 2 3,arguments与命名参数不再同步
- 箭头函数不能访问
arguments
,只能访问命名参数
let bar2 = (num1, num2) => {
// console.log(arguments[0], arguments[1]) // Uncaught ReferenceError: arguments is not defined
console.log(num1, num2)
}
bar2(2, 3) // 2 3
- JS 中的所有参数都是按值传递的,如果把对象作为参数传递,传递的值是这个对象的引用(仍是按值传递)
10.4 没有重载
- JS 的函数没有签名,因此没有重载,后定义的同名函数会覆盖先定义的
function addSomeNumber(num) {
return num + 100
}
function addSomeNumber(num) {
return num + 200
}
let result = addSomeNumber(100)
console.log(result) // 300
// 把函数名当成指针
let addSomeNumber2 = function (num) {
return num + 100
}
addSomeNumber2 = function (num) {
return num + 200
}
let result2 = addSomeNumber2(100)
console.log(result2) // 300
10.5 默认参数值
- ES5 及以前,实现默认参数的常用方法是检测某个参数是否等于
undefined
function makeKing(name) {
name = typeof name !== 'undefined' ? name : 'Henry' // 检测参数name是否为undefined,如果是则赋值
return `King ${name} VIII`
}
console.log(makeKing()) // 'King Henry VIII'
console.log(makeKing('James')) // 'King James VIII'
- ES6 及以后支持显式定义默认参数,在函数定义中的参数后赋值即可
function makeKing2(name = 'Henry') {
return `King ${name} VIII`
}
console.log(makeKing2()) // 'King Henry VIII'
console.log(makeKing2('James')) // 'King James VIII'
- 使用默认参数时,
arguments
对象不反映参数默认值,只反映传给函数的参数,修改命名参数不会影响arguments
对象
function makeKing3(name = 'Henry') {
name = 'Louis' // 修改命名参数
return `King ${arguments[0]}`
}
console.log(makeKing3()) // 'King undefined',传给函数的参数为undefined
console.log(makeKing3('James')) // 'King James',传给函数的参数为'James'
- 可以使用调用函数的返回值作为默认参数值,计算默认值的函数在未传相应参数时被调用
let romanNumerals = ['Ⅰ', 'Ⅱ', 'Ⅲ', 'Ⅳ', 'Ⅴ', 'Ⅵ']
let ordinality = 0
function getNumerals() {
return romanNumerals[ordinality++] // 每次调用后递增
}
function makeKing4(name = 'Henry', numerals = getNumerals()) {
return `King ${name} ${numerals}`
}
console.log(makeKing4()) // 'King Henry Ⅰ',未传numerals参数,调用函数getNumerals()
console.log(makeKing4('James', 'Ⅸ')) // 'King James Ⅸ',已传numerals参数,不调用函数
console.log(makeKing4()) // 'King Henry Ⅱ',未传numerals参数,调用函数getNumerals()
console.log(makeKing4()) // 'King Henry Ⅲ',未传numerals参数,调用函数getNumerals()
- 箭头函数同样可以使用默认参数,只有一个参数时不能省略括号
let makeKing5 = (name = 'Henry') => `King ${name}`
console.log(makeKing5()) // 'King Henry'
默认参数作用域与暂时性死区
- 给参数定义默认值,实际上相当于使用
let
关键字按顺序声明变量一样
function makeKing6(name = 'Henry', numerals = 'Ⅷ') {
return `King ${name} ${numerals}`
}
// 使用let按顺序声明变量
function makeKing7() {
let name = 'Henry'
let numerals = 'Ⅷ'
return `King ${name} ${numerals}`
}
- 后定义默认值的参数可以引用先定义的参数;反之会因为暂时性死区报错(
let
声明的变量不会在作用域中被提升)
function makeKing8(name = 'Henry', numerals = name) {
return `King ${name} ${numerals}`
}
console.log(makeKing8()) // 'King Henry Henry'
function makeKing9(name = numerals, numerals = 'Ⅷ') {
return `King ${name} ${numerals}`
}
// console.log(makeKing9()) // ReferenceError: Cannot access 'numerals' before initialization
- 参数存在自己的作用域,不能引用函数体的作用域
function makeKing10(name = 'Henry', numerals = defaultNumeral) {
let defaultNumeral = 'Ⅷ'
return `King ${name} ${numerals}`
}
// console.log(makeKing10()) // ReferenceError: defaultNumeral is not defined
10.6 参数扩展与收集
- ES6 新增扩展操作符
...
,可以用于调用函数时传参和定义函数参数
10.6.1 扩展参数
- 可对可迭代对象应用扩展操作符并将其作为一个参数传入,会将可迭代对象拆分并传入迭代返回的每个值
let values = [1, 2, 3, 4]
function getSum() {
let sum = 0
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i]
}
return sum
}
console.log(getSum(...values)) // 10,1+2+3+4
- 可在扩展操作符前面、候面再传其他的值
console.log(getSum(-1, ...values)) // 9,-1+1+2+3+4
console.log(getSum(...values, 5)) // 15,1+2+3+4+5
console.log(getSum(-1, ...values, 5)) // 14,-1+1+2+3+4+5
console.log(getSum(...values, ...[5, 6, 7])) // 28,1+2+3+4+5+6+7
arguments
对象仍按照调用函数时传入的参数接收每一个值
function countArgs() {
console.log(arguments.length)
}
countArgs(-1, ...values) // 5
countArgs(...values, 5) // 5
countArgs(-1, ...values, 5) // 6
countArgs(...values, ...[5, 6, 7]) // 7
- 使用扩展操作符的同时,也可以使用默认参数
function getProduct(a, b, c = 1) {
return a * b * c
}
console.log(getProduct(...[1, 2])) // 2,1*2*1
console.log(getProduct(...[1, 2, 3])) // 6,1*2*3
console.log(getProduct(...[1, 2, 3, 4])) // 6,1*2*3
let getSum2 = (a, b, c = 0) => {
return a + b + c
}
console.log(getSum2(...[0, 1])) // 1,0+1
console.log(getSum2(...[0, 1, 2])) // 3,0+1+2
console.log(getSum2(...[0, 1, 2, 3])) // 3,0+1+2
10.6.2 收集参数
- 可以使用扩展操作符把不同长度的独立参数组合为一个数组
function getSum3(...values) {
return values.reduce((pre, cur) => pre + cur, 0)
}
console.log(getSum3(1, 2, 3)) // 6
- 因为收集参数的结果可变,因此只能把它作为最后一个参数
- 收集参数的前面如果还有命名参数,则只收集其余参数;若没收集到则获得空数组
function getProduct(...values, lastValue) {} // SyntaxError: Rest parameter must be last formal parameter
function ignoreFirst(firstValue, ...values) {
console.log(values)
}
ignoreFirst() // [],没收集到
ignoreFirst(1) // [],没收集到
ignoreFirst(1, 2) // [2],收集其余参数
ignoreFirst(1, 2, 3) // [2, 3],收集其余参数
- 箭头函数支持收集参数的定义方式,可用其实现与使用
arguments
一样的逻辑
let getSum4 = (...values) => values.reduce((pre, cur) => pre + cur, 0)
console.log(getSum4(1, 2, 3, 4)) // 10
- 收集参数不影响
arguments
对象,仍反映调用时传给函数的参数
function getSum5(...values) {
console.log(arguments.length) // 4
console.log(arguments) // [Arguments] { '0': 1, '1': 2, '2': 3, '3': 4 }
console.log(values) // [ 1, 2, 3, 4 ]
}
getSum5(1, 2, 3, 4)
10.7 函数声明与函数表达式
- js 引擎在代码开始执行之前,解析器通过函数声明提升(function declaration hoisting)的过程,将声明函数放到源代码树的顶部,使其在执行任何代码之前可用(可以访问);而函数表达式则必须等到解析器执行到所在代码行才被解释执行。
- 函数声明和函数表达式的唯一区别就是什么时候可以通过变量访问函数
console.log(sumDeclare(10, 10)) // 函数声明会提前
function sumDeclare(num1, num2) {
return num1 + num2
}
console.log(sumExpression(10, 10)) // 函数表达式不会提前,会报错,sumExpression is not a function
let sumExpression = function (num1, num2) {
return num1 + num2
}
10.8 函数作为值
- 函数名在 ECMAScript 中是变量,函数即可作为另一个函数的参数,也可在一个函数中返回另一个函数
- 如果是访问函数指针而非调用函数,必须不带括号
function callSomeFunction(someFunction, someArgument) {
return someFunction(someArgument)
}
function add10(num) {
return num + 10
}
let result3 = callSomeFunction(add10, 10) // 访问函数的指针而不是执行函数,add10不带括号
console.log(result3) // 20
function getGreeting(name) {
return 'Hello,' + name // Hello,Nicholas
}
let result4 = callSomeFunction(getGreeting, 'Nicholas') // 访问函数的指针而不是执行函数,getGreeting不带括号
console.log(result4) // 'Hello,Nicholas'
- 根据数组对象的某个对象属性进行排序:定义一个根据属性名来创建比较函数的函数
function arraySort(key, sort) {
return function (a, b) {
if (sort === 'asc' || sort === undefined || sort === '') {
// 正序:a[key] > b[key]
if (a[key] > b[key]) return 1
else if (a[key] < b[key]) return -1
else return 0
} else if (sort === 'desc') {
// 倒序:a[key] < b[key]
if (a[key] < b[key]) return 1
else if (a[key] > b[key]) return -1
else return 0
}
}
}
var userList = [
{ name: 'Tony', id: 3 },
{ name: 'Tom', id: 2 },
{ name: 'Jack', id: 5 },
]
console.log(userList.sort(arraySort('id'))) // [{ name: 'Tom', id: 2 },{ name: 'Tony', id: 3 },{ name: 'Jack', id: 5 }],按 id 正序排列
console.log(userList.sort(arraySort('id', 'desc'))) // [{ name: 'Jack', id: 5 },{ name: 'Tony', id: 3 },{ name: 'Tom', id: 2 }],按 id 倒序排列
console.log(userList.sort(arraySort('name'))) // [{ name: 'Jack', id: 5 },{ name: 'Tom', id: 2 },{ name: 'Tony', id: 3 }],按 name 正序排列
总结 & 问点
- 函数是什么?函数有哪几种定义方式?
- 箭头函数在什么情况下可以不用参数的括号?什么情况下可以不用函数体的大括号?
- 箭头函数为什么不能用作构造函数或定义原型方法?
- 函数名是什么?一个函数可以有多少函数名?如何获取函数标识符?
- 函数内部的 arguments 对象是什么?其使用有哪些特点和限制?
- 如何理解并证明 JS 的函数没有重载?
- ES5 及之前、ES6 及之后,分别如何定义函数的默认参数?
- arguments 对象与函数默认参数有什么关联?如何理解默认参数的作用域与暂时性死区?
- 扩展操作符有什么作用?定义参数时使用其对 arguments 有什么影响?
- 箭头函数如何实现与 arguments 一样的逻辑,获取每个参数?
- 函数声明与函数表达式有什么区别?
- 写一段代码,根据对象数组的某个对象属性进行排序,可根据参数决定排序属性及升/降序
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。