【重走JavaScript之高级程序设计】函数Function

函数

函数实际上是对象,每个函数都是Function类型的实例。函数名是指向函数对象的指针。

函数的声明方式通常有两种,没有什么区别。但是不推荐new Function()写法。函数的声明表达式会在编译时提升,而函数表达式则不会提升变量。

  1. 函数表达式
function sum (sum1, sum2){
    return sum1 + sum2
}
  1. 函数声明的方式
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类数组
  1. 因为函数的参数在内部表现为一个数组,arguments对象。可以使用[]语法来访问其中的元素(第一个参数arguments[0]为第一个参数)
  2. arguments对象的长度根据传入的参数绝大,而非定义时的参数个数。arguments.length可以确定传进来多少个参数。
  3. arguments可以和命名参数一起使用。如果没有传入参数,arguments则为undefined。
  4. 箭头函数中不能使用arguments对象。
3.默认参数

定义形参时直接用=号赋值一个默认值,这种参数就叫默认参数

  1. 默认参数的使用
function makeName(firstName = 'paul',secondName = 'walker'){
    return firstName + secondName
}

makeName()  // paulwaler

makeName(undefined, 'chris')    // paulchris 给前面的参数传入undefined,可以更巧妙的利用默认值
  1. arguments对象不反应参数的默认值,即arguments值反应传入函数的参数

  2. 默认参数也可以使用调用函数的返回值,箭头函数也可以使用默认值

function getSecName(){
    return 'walker'
}
function y(firstName = 'paul',secondName = getSecName() ){
    return firstName + secoundName
}
y()     // paulwaker
  1. 后定义的默认参数可以引用先定义的参数,因为给多个参数定义默认值实际上跟使用let顺序声明变量一样。反过来则不行
function xxx(a = 'paul', b = a){
    return a + b
}

function xxx()  // paulpaul
4、扩展参数与收集参数

扩展参数:实参,通过扩展操作符消费一组数组,迭代数组中的值依次传入

收集参数:形参,通过扩展操作符把独立的参数收集为一个数组。类似arguments

  1. 扩展参数(扩展操作符),这里函数调用时使用(实参)
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
  1. 收集参数(剩余参数),这里函数定义时使用(形参)。收集参数可以和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]
  1. 箭头函数不支持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的指向。
区别:

  1. call和apply的使用仅仅是函数传参的区别,call传入的是单个参数,apply传入的是一个数组
  2. 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() 
}

条件:

  1. 严格模式下use strict
  2. 外部函数的返回值是对尾调用函数的调用
  3. 尾调用函数返回不要执行额外的逻辑
  4. 尾调用函数没有引用外部函数作用域中自由变量,而形成闭包
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
}

十、函数的总结

  1. 函数表达式与函数声明是不一样的。函数要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式会被称为匿名函数
  2. 箭头函数的语法,还有箭头函数和普通函数的区别(this,arguments)
  3. JavaScript中函数定义与调用时的参数及其灵活。使用arguments对象与扩展操作符,可以实现函数定义和调用的完全动态化。
  4. JavaScript引擎可以优化符合尾调用条件的函数,以节省栈空间。
  5. 闭包的作用域链包含自己的变量对象,然后是包含函数的变量对象,直到全局上下文的变量的对象(从内到外)。
  6. 通常,函数作用域及其所有变量在函数执行完毕后都会销毁,闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
  7. 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用(IFFE),,不过let已经革命性的解决这个问题了。
  8. 虽然JavaScript没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域中定义的变量。可以访问私有变量的公共方法叫做特权方法。
  9. 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增强模式在单例对象上实现。
posted @   wanglei1900  阅读(30)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示