JavaScript 中的函数

JavaScript 中的函数

一般来说,一个函数是可以通过外部代码调用的一个子程序(或在递归的情况下由内部函数调用),像程序本身一样,一个函数由称为函数体的一系列语句组成,值可以传递给一个函数,函数也可以返回一个值

函数的定义

JavaScript 中,函数是头等(first-class)对象,因为它们可以像任何其他对象一样具有属性和方法,它们与其他对象的区别在于函数可以被调用,简而言之,它们是 Function 对象,下面是一个简单的函数定义方式

1
2
3
4
5
function fn1() {
alert('fn1')
}

typeof fn1 // function

JavaScript 中函数就是对象,只不过函数是一个非常特殊的对象,是一个 Function 类的实例,其实在内存中存储的操作是通过一个键值对来存储的,函数的名称仅仅是一个键,它的值是指向了内存中的一个对象,这个对象就是 Function 的一个对象,我们又声明一个变量 fn2,它是通过函数的拷贝来完成赋值的,两个引用并没有指向同一个对象

1
2
3
4
5
6
7
8
9
10
11
12
var fn2 = fn1

// fn1 两个的值是虽然相等,但是指向不同的空间
fn2()

// fn1 的值改变了以后,不会影响 fn2 的值
fn1 = function () {
alert('fnn1')
}

fn2() // 这里修改了 fn1 的内容,fn2 未做修改,故还是 fn1
fn1() // fnn1

函数虽然是一个对象,但是却和对象有一些区别,对象是通过引用的指向完成对象的赋值的,而函数确实通过对象的拷贝来完成的,所以 fn1 虽然改变了,但是并不会影响 fn2,对于对象而言,是通过引用的指向来完成赋值的,此时修改 o1 或者 o2 会将两个值都完成修改

1
2
3
4
5
6
// o1 和 o2 其实指向了同一块空间,当修改 o2 的值的时候,o1 的也会改变
var o1 = new Object()
var o2 = o1

o2.name = 'abc'
alert(o1.name) // abc

函数对象

对象字面量产生的对象连接到 Object.prototype, 函数对象连接到 Function.prototype(该原型对象本身连接到 Object.prototype

1
Function.prototype.__proto__ === Object.prototype  // true

每个函数在创建的时候会有两个附加属性,函数上下文和实现函数行为的代码,每个函数对象在创建的时候也会带有一个 constructor 属性,它的值是一个拥有 constructor 属性且值即为该函数的对象

函数调用

调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数,除了声明的时候定义的形式参数,每个函数接收两个附加的参数 thisarguments,参数 this 的值取决于调用的模式,在 JavaScript 中一共有四种调用模式

  • 方法调用模式
  • 函数调用模式
  • 构造调用模式
  • apply/call 调用模式

当实际参数(arguments)的个数与形式参数(parameters)的个数不匹配的时候不会导致程序错误,主要分为以下几种情况

  • 如果实际参数过多,超出的参数值将会被忽略
  • 如果实际参数过少,缺失的部分将会被替换为 undefined
  • 而且需要注意的是,对参数的值不会进行类型检查,即任何类型的值都可以被传递给参数

函数没有重载

比如下面这个例子

1
2
3
4
5
6
7
8
9
10
function sum(num1, num2) {
return num1 + num2
}

function sum(num1) {
return num1 + 100
}

sum(10) // 110
sum(10, 20) // 110

换一种写法,可以看的更清楚些

1
2
3
4
5
6
7
8
9
10
var sum = function (num1, num2) {
return num1 + num2
}

var sum = function (num1) {
return num1 + 100
}

sum(10) // 110
sum(10, 20) // 110

可以发现,下面一个 sum 方法会将上面的同名方法覆盖掉,所以只会执行下面的方法,特别指出,函数的参数和调用没用关系,如果函数只有一个参数,但是却传入了两个参数,仅仅只会匹配一个,函数是对象,不存在重载,只会存在覆盖,后面定义的会覆盖前面所定义的

1
2
3
4
5
6
7
8
9
10
11
/**
* 如下定义方式等于定义了一个
* function fn(num1, num2) {
* alert(num1 + num2)
* }
*
* 所以通过以下的例子,充分说明了函数就是一个对象
*/
var fn = new Function('num1', 'num2', 'alert(num1 + num2)')

fn(12, 22) // 34

函数的值传递

由于函数是对象,所以可以直接把函数通过参数传递进来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function callFun(fun, arg) {
// 第一个参数就是函数对象
return fun(arg)
}

function sum(num) {
return num + 100
}

function say(str) {
alert(str)
}

// 调用了 say 函数
callFun(say, 'abc')

// 调用了 sum 函数
callFun(sum, 20)

作为返回值来传递(也就是所谓的闭包)

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn1(arg) {
var rel = function (num) {
return arg + num
}
return rel
}

// 此时 f 是一个函数对象,可以完成调用
// 在外部调用的时候,更改函数内部的值
var f = fn1(20)

f(20) //40
f(11) //31

函数中的 arguments

在函数对象中有一个属性叫做 arguments,通过这个属性可以获取相应的参数值,这个属性是一个类数组对象,其实就是传递进来的参数的一个集合

1
2
3
4
5
6
function say(num) {
alert(num) // 1
alert(arguments.length) // 3
}

say(1, 2, 3)

arguments.callee()

arguments 这对象中有一个 callee 的方法,arguments.callee(arg) 可以实现反向的调用(已经不再推荐使用)

1
2
3
4
5
6
7
8
9
10
function factorial(num) {
if (num <= 1) {
return 1
} else {
// 此时和函数名耦合在一起
return num * factorial(num - 1)
}
}

factorial(5) // 120

以上是一个求阶乘的函数,递归调用的函数名称和原有函数名耦合在一起,如果将这个函数名称更改以后,递归就会失效

1
2
3
4
5
6
7
8
var fn = factorial

// 此时不会报错
fn(5) // 120

factorial = null

fn(5) // 报错

此时由于 fn 函数依然使用 factorial 这个名称来调用,但是 factorial 已经指向 null 了,所以会报错,如上情况就需要使用 arguments.callee 方法来调用

1
2
3
4
5
6
7
8
9
10
11
function factorial(num) {
if (num <= 1) {
return 1
} else {
// 以下就实现了函数名的解耦,在 JavaScript 中通常都是使用这种方式来做递归
return num * arguments.callee(num - 1)
}
}


fn(5) // 120

函数中的 this

当需要创建一个函数对象的时候,设置函数对象的属性和方法需要通过 this 关键字来引用,但是特别注意 this 关键字在调用的时候会根据不同的调用对象而变得不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var color = 'red'

function showColor() {
alert(this.color)
}

function Circle(color) {
this.color = color
this.showColor = showColor
}

var c = new Circle('yellow')

c.showColor() // yellow
showColor() // red

使用 c 来调用 showColor 方法,等于调用了 showColor 方法,此时的 thisc,所以是 yellow,直接调用 showColor,此时调用的对象等于是 windowshowColorthis 就是 window,所以就会在 window 中寻找 color

函数的返回值

当一个函数被调用的时候,它从第一个语句开始执行,并在遇到关闭函数体的 } 时结束,从而是的函数把控制权交还给调用该函数的程序部分,return 语句可用来使函数提前返回,当 return 被执行的时候,函数立即返回而不再执行余下的语句,一个函数总是有一个返回值,如果没有指定返回值,则返回 unfefined

如果函数在前面加上 new 前缀的方式来调用,且返回值不是一个对象,则返回 this(该新对象),简单的总结就是,如果一个构造函数不写 return 语句,则系统会自动帮你返回一个对象,但是如果写了 return 语句,则

  • 如果 return 的是一个基本类型的值,则会忽略这个 return,该返回什么还是返回什么,但是会阻止构造函数接下来的执行
  • 如果 return 了一个引用类型,则原有的 return 会被覆盖

一些函数的实例

下面我们来几个比较常见的排序的示例,在平常开发当中也是经常会遇到的

实例一

1
2
3
4
5
6
7
8
function sortByNum(a, b) {
return a - b
}

var arr = [1, 2, 11, 33, 12, 190]

arr.sort() // 1, 11, 12, 190, 2, 33
arr.sort(sortByNum) // 1, 2, 11, 12, 33, 190

实例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Person(name, age) {
this.name = name
this.age = age
}

var p1 = new Person('zhangsan', 12)
var p2 = new Person('lisi', 15)
var p3 = new Person('wangwu', 18)

var ps = [p1, p2, p3]

function sortByName(obj1, obj2) {
if (obj1.name > obj2.name) {
return 1
} else if (obj1.name == obj2.name) {
return 0
} else {
return -1
}
}

function sortByAge(obj1, obj2) {
return obj1.age - obj2.age
}

ps.sort(sortByName)
ps.sort(sortByAge)

使用如上的方式来处理排序,带来的问题是需要为每一个属性都设置一个函数,显得不是很灵活,所以可以通过函数的返回值来调用,实现函数排序功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function sortByProperty(propertyName) {
var sortFun = function (obj1, obj2) {
if (obj1[propertyName] > obj2[propertyName]) {
return 1
} else if (obj1[propertyName] == obj2[propertyName]) {
return 0
} else {
return -1
}
}
}

ps.sort(sortByProperty('name'))
ps.sort(sortByProperty('age'))

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×