【重走JavaScript之高级程序设计】函数Function
函数
函数实际上是对象,每个函数都是Function类型的实例。函数名是指向函数对象的指针。
函数的声明方式通常有两种,没有什么区别。但是不推荐new Function()写法。函数的声明表达式会在编译时提升,而函数表达式则不会提升变量。
- 函数表达式
function sum (sum1, sum2){
return sum1 + sum2
}
- 函数声明的方式
let sum = function(sum1, sum2){
return sum1 + sum2
}
一、箭头函数
ES6新增了使用 ( => ) 的语法定义函数表达式的能力
let sum = (sum1,sum2) => {
return sum1 + sum2
}
如果只有一个参数,可以不写()。只有没有参数或者多个参数的情况下,才需要使用括号。
let result = a => {
return a*2
}
如果只有一行代码,可以省略return和{}。这是因为箭头函数省略大括号会隐式的返回这行代码的值。
let result = a => a*2
箭头函数的注意点,没有arguments、super和new.target,也不能作构造函数。箭头函数也没有prototype属性
二、函数名
函数名就是指向函数的指针。
因为函数只是个指针,其实一个函数可以有多个名称。使用不带名字的函数名会访问函数指针但是不会执行函数。
function sum(sum1,sum2){
return sum1 + sum2
}
let anotherSum = sum
sum = null
console.log(anotherSum(5,5)) //
ES6的所有函数对象都会暴露一个只读的name属性。
fuction foo(){}
let bar = function(){}
let baz = () => {}
console.log(foo.name) // foo
console.log(bar.name) // bar
console.log(baz.name) // (空字符串)
console.log((new Function()).name) // anonymous
console.log(foo.bind(null).name) // bound foo
三、函数的参数
JavaScript的参数和绝大部分的语言不同,既不关心传入的参数个数,也不关心参数的类型。函数的参数有命名参数,调用函数参数,arguments, 默认参数,扩展参数,收集参数。
JS中所有参数都是按值传递的,不可能按引用传递参数。如果把参数包装成对象传入,传递的值就为这个对象的引用。
1.命名参数
常见的形参使用就属于命名参数
2.arguments类数组
- 因为函数的参数在内部表现为一个数组,arguments对象。可以使用[]语法来访问其中的元素(第一个参数arguments[0]为第一个参数)
- arguments对象的长度根据传入的参数绝大,而非定义时的参数个数。arguments.length可以确定传进来多少个参数。
- arguments可以和命名参数一起使用。如果没有传入参数,arguments则为undefined。
- 箭头函数中不能使用arguments对象。
3.默认参数
定义形参时直接用=号赋值一个默认值,这种参数就叫默认参数
- 默认参数的使用
function makeName(firstName = 'paul',secondName = 'walker'){
return firstName + secondName
}
makeName() // paulwaler
makeName(undefined, 'chris') // paulchris 给前面的参数传入undefined,可以更巧妙的利用默认值
-
arguments对象不反应参数的默认值,即arguments值反应传入函数的参数
-
默认参数也可以使用调用函数的返回值,箭头函数也可以使用默认值
function getSecName(){
return 'walker'
}
function y(firstName = 'paul',secondName = getSecName() ){
return firstName + secoundName
}
y() // paulwaker
- 后定义的默认参数可以引用先定义的参数,因为给多个参数定义默认值实际上跟使用let顺序声明变量一样。反过来则不行
function xxx(a = 'paul', b = a){
return a + b
}
function xxx() // paulpaul
4、扩展参数与收集参数
扩展参数:实参,通过扩展操作符消费一组数组,迭代数组中的值依次传入
收集参数:形参,通过扩展操作符把独立的参数收集为一个数组。类似arguments
- 扩展参数(扩展操作符),这里函数调用时使用(实参)
let num = [1,2,3,4]
function xxx(){
console.log(arguments.length)
}
xxx(...num) // 4
xxx(1, ...num) // 5
xxx(...num, 2, 2) // 6
xxx(...num, ...[1,2,3]) // 7
function getSum(a, b, c = 0){
return a + b + c
}
getSum(...[1,2,3]) // 6
- 收集参数(剩余参数),这里函数定义时使用(形参)。收集参数可以和arguments对象一起使用。
// function getSum(...values,a){} // 剩余参数顾名思义,不能定义在其他参数前
function getSum(a,...values){
console.log(...values)
}
getSum() // []
getSum(1) // []
getSum(1, 2) // [2]
getSum(1, 2, 3) // [2,3]
- 箭头函数不支持arguments对象,但支持收集参数
let getSum = (...values) => {
console.log(values)
}
getSum(1,2,3) // [1,2,3]
四、函数作为值
把函数作为参数传入
// 这个函数时把一个函数return出去并把someArguments传入
function callSomeFunction(someFunction, someArguments){
return someFunction(someArguements)
}
function add10(num){
return num + 10
}
let result = callSomeFunction(add10, 10) // 20
从一个函数中返回另一个函数,进行数组中某个属性的排序
let arr = [
{name:'w',age:19},
{name:'w',age:15},
{name:'w',age:25},
{name:'w',age:7},
]
function sortAge(property){
return function(obj1, obj2){
let value1 = obj1[property]
let value2 = obj2[property]
if(value1 < value2){
return -1
}else if(value1 < value2){
return 1
}else{
return 0
}
}
}
arr.sort(sortAge('age'))
五、函数内部的属性
1. arguments对象
arguments为调用函数时传入的所有参数(实参),是一个类数组对象
arguments对象还有一个callee属性,是一个指向arguments对象所在函数的指针(函数名)(仅了解,官方已经推荐,要么给函数表达式一个名字,要么使用一个函数声明)看第七条
function factorial(num){
if(num <= 1){
return 1
}else{
return num * factorial(num - 1)
}
}
// 利用arguments.callee将函数逻辑与函数名解耦
function factorial(num){
if(num <= 1){
return 1
}else{
return num * arguments.callee(num - 1)
}
}
let trueFactorial = factorial // 将同一个函数的指针另外保存一份到trueFactorial这个位置
factorial = function(){ return 0 } // 重写factorial函数
trueFactorial(5) // 120 函数逻辑和名称已经解耦,随便更改为什么名字都可以正确计算
factorial(5) // 0
2.this
this是把函数当成方法调用的上下文对象。
箭头函数中,this是定义箭头函数的上下文。
window.col = 'red';
let o = {
col:'blue'
}
funciton sayCol(){
console.log(this.col)
}
sayColor() // red
o.sayCol = sayCol
o.sayCol() // blue
以上例子,函数名只是保存指针的变量。因此全局定义的sayCol()函数和o.sayCol()函数是同一个函数,只不过执行上下文不用
window.col = 'red';
let o = {
col:'blue'
}
let sayCol = () => { console.log(this.col) }
sayColor() // red
o.sayCol = sayCol
o.sayCol() // red
六、函数属性与方法
函数也是对象,也有属性和方法。每个函数有两个属性,length和prototype。length是函数定义命名参数(形参)的个数
prototype是保存引用类型所有实例方法的地方。这里不细讲。
函数还有其他的方法,call(), apply(), bind()
call(), apply(), bind(),这三个方法都会以指定传入的this值来调用函数。都可以强行改变this的指向。
区别:
- call和apply的使用仅仅是函数传参的区别,call传入的是单个参数,apply传入的是一个数组
- bind方法会创建一个新的函数实例,this的值会绑定传给bind的对象
window.col = 'red';
let o = {
col:'blue'
}
funciton sayCol(){
console.log(this.col)
}
sayColor() // red
sayColor().call(this) // red
o.sayCol = sayCol
o.sayCol() // blue
o.sayCol().call(window) // red
window.col = 'red';
let o = {
col:'blue'
}
funciton sayCol(){
console.log(this.col)
}
let newSayCol = sayCol.bind(o)
newSayCol() // blue
七、函数的递归
递归函数是一个函数通过名称调用自己
// 放弃使用arguments.callee 来递归,而是使用函数声明来达到同样的效果
const factorial = ( function f(num){
if(num <= 1){
return 1
}else{
return num * f(num -1)
}
})
八、函数的尾调用优化
外部函数的栈帧没有存在的必要情况下直接跳过外部函数的栈帧
function outerFunction(){
return innerFunction() // 尾调用,ES6将直接对尾调用优化
}
funciton outerFn(){
return condition ? innerFnA() : innerFnB()
}
条件:
- 严格模式下use strict
- 外部函数的返回值是对尾调用函数的调用
- 尾调用函数返回不要执行额外的逻辑
- 尾调用函数没有引用外部函数作用域中自由变量,而形成闭包
function fib(n){
if(n < 2){
return 2
}
return fib(n - 1) + fib(n - 2)
}
以上函数函数不符合尾调用优化,fib(n)的栈帧数的内存复杂度是O(2的n次方),浏览器会有很大的损耗。
"use strice"
function fib(n){
return fibImpl(0, 1, n);
}
functiuon fibImpl(a, b, n){
if(n === 0){
return a
}
return fibImpl(b, a + b, n - 1);
}
九、函数的闭包
嵌套函数中,内部函数中引用了外部函数的变量
由于有作用域链,内部函数的生命周期一直存在着。
function sortAge(property){
return function(obj1, obj2){
let value1 = obj1[property] // 嵌套函数,内部匿名函数引用外部函数的变量(参数)
let value2 = obj2[property]
if(value1 < value2){
return -1
}else if(value1 < value2){
return 1
}else{
return 0
}
}
}
作用域链其实是一个包含函数指针的列表,每个指针指向一个变量对象。内部匿名函数引用了外部函数的变量(参数),内部匿名函数的作用域链包含了匿名函数本身、外部函数、全局变量对象。当外部函数执行完毕,由于内部匿名函数的作用域链中仍然对外部函数有引用,所以构成了闭包。
解决办法是手动将函数的引用置为null,这样将切断作用域链,随之闭包会进行销毁。
1.this对象
闭包里使用this会造成混乱
window.id = 'The window'
let obj = {
id:'My Object',
getId(){
return function(){
return this. id
}
}
}
obj.getId()() // 'The window'
内部函数无法访问外部函数的this和arguments,除非把this保存到另一个变量中
window.id = 'The window'
let obj = {
id:'My Object',
getId(){
const that = this
return function(){
return that. id
}
}
}
obj.getId()() // 'My Object'
2.内存泄漏
闭包会造成浏览器无法对其使用垃圾回收,而导致内存泄漏,切断作用域链
作用域和内存一章已经讲的很清楚了不赘述了。
十、函数的私有变量
函数的私有变量包括函数参数、局部变量、以及函数内部定义的其他函数
function add(num1, num2){
let sum = num1 + num2;
return sum
}
以上函数有三个私有变量,num1、num2和sum,这几个变量只能在函数内部访问,不能在函数外部访问。但是闭包可以通过其作用域链访问外部的这三个私有变量。这种能够访问私有变量的方法,就叫特权方法。
function MyObject(){
// 私有变量
let privateVar = 10
// 私有函数
function privateFn(){
return false
}
// 特权方法访问私有变量和私有函数,只能通过调用publicMethod方法
// 这里构成了闭包,嵌套函数,嵌套函数内引用外部函数的变量
this.publicMethod = function(){
privateVar++
return privateFn()
}
}
以下这段diamante定义了两个特权方法,getName(),setName(),这两个方法都可以通过构造函数外部调用并通过它们读写私有的name变量。
// 函数的参数name就是私有变量
function Person(name){
this.getName = function(){
return name
}
this.setName = function(value){
name = value
}
}
let person = new Person('paul')
person.getName() // 'paul'
person.setName('waler')
person.getName() // 'walker'
以上代码,参数name对于构造函数的实例都是独立的,构造函数的缺点是每个实例都会重新创建一遍新方法,使用静态私有变量实现特权可以避免这个问题。
1.静态私有变量
这里通过原型的方式,实现了私有变量和私有函数由实例共享。
(function(){
// 私有变量
let privateVar = 10
// 私有函数
function privateFn(){
return false
}
// 构造函数
MyObject = function(){}
MyObject.prototype.publicMethod = function(){
privateVar++
return privateFn()
}
})()
以下代码通过Person构造函数访问私有变量name,在这种模式下,私有变量变为静态私有变量,可供所有实例使用(共享)。
(function(){
let name = ''
Person = function(value){
name = value
}
Person.prototype.getName = function(){
return name
}
Person.prototype.setName = function(){
name = value
}
})()
let person1 = new Person('paul')
person1.getName() // 'paul'
person1.setName('walker')
person1.getName() // 'walker'
let person2 = new Person('tom')
person1.getName() // 'tom'
person2.getName() // 'tom'
两种方式,私有变量和静态私有变量的使取决于自己的需求,是否需要对私有变量进行实例间的共享还是隔离
2.模块模式
模块模式中,单例对象作为一个模块,经过初试化自定义可以包含某些私有数据,而这些私有数据又可以通过其暴露的公共方法来访问。
单例对象,只有一个实例的对象,JS通过对象字面量来创建单例对象。
模块模式则是在单例对象的基础上加以扩展,通过作用域链来关联私有变量和特权方法。
let singleton = function(){
let privateVar = 10
function privateFn(){
return false
}
// 特权(公有)方法和属性
return {
publicProperty:true
publicMethod(){
privateVar++
return privateFn()
}
}
}
以上代码,外层函数定义了私有变量和私有函数,并返回一个对象字面量,这个对象字面量中只包含了可以公开访问的属性和方法。因为这个对象定义在函数内部,对象中的公有方法可以访问同一个作用域,外层函数的私有变量和私有函数。
对象字面量定义了单例对象的公共接口
如果单例对象需要某种初始化,并且需要访问私有变量,可以采用以下模式
let application = function(){
// 定义私有变量
let components = new Array()
// 初始化(BaseComponent仅仅举例)
components.push(new BaseComponent())
// 定义公共接口
return {
getComponents(){
return components.length
}
registerComponent(component){
if(typeof component === 'object'){
components.push(component)
}
}
}
}
以上这个例子,创建了一个application对象管理组件,创建私有数组components,然后将一个BaseComponent实例添加到数组。对象字面量中定义的getComponents()和registerComponent()都可以访问components私有数组的特权方法。
3.模块增强模式
和模块模式相比,模块增强模式指的是返回一个实例对象,可以添加额外属性或方法
let singleton = function(){
let privateVar = 10;
function privateFn(){
return false
}
// 创建实例对象
let obj = new CustomType();
// 添加特权/公有的属性和方法
obj.publicProperty = true;
obj.publicMethod = function(){
privateVar++
return privateFn()
}
// 返回实例对象以便添加额外属性或方法
return obj
}
用模块增加模式重写之前那个application单例对象
let application = function(){
// 定义私有变量
let components = new Array()
// 初始化(BaseComponent仅仅举例)
components.push(new BaseComponent())
// 创建局部变量保存实例
let app = new BaseComponent()
// 定义公共接口
app.getComponentCount = function() {
return components.length
}
app.registerComponent = function(component){
if(typeof component === 'object'){
components.push(component)
}
}
// 返回实例
return app
}
十、函数的总结
- 函数表达式与函数声明是不一样的。函数要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式会被称为匿名函数
- 箭头函数的语法,还有箭头函数和普通函数的区别(this,arguments)
- JavaScript中函数定义与调用时的参数及其灵活。使用arguments对象与扩展操作符,可以实现函数定义和调用的完全动态化。
- JavaScript引擎可以优化符合尾调用条件的函数,以节省栈空间。
- 闭包的作用域链包含自己的变量对象,然后是包含函数的变量对象,直到全局上下文的变量的对象(从内到外)。
- 通常,函数作用域及其所有变量在函数执行完毕后都会销毁,闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
- 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用(IFFE),,不过let已经革命性的解决这个问题了。
- 虽然JavaScript没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域中定义的变量。可以访问私有变量的公共方法叫做特权方法。
- 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增强模式在单例对象上实现。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现