[ JavaScript ] 函数
1. 函数的定义及调用
1.1 定义函数
function abs(x) { if (x >= 0) { return x; } else { return -x; } }
function: 定义函数关键字;
abs: 函数名
(x): x 为函数的参数,多个参数以 , 分隔
{ ... }: 之间的代码块是函数体
函数体内部的语句在执行时,一旦执行到 return 时,函数就执行完毕,并将结果返回。如果没有 return 语句,函数执行完毕后也会返回结果,只有结果为 undefined
第二种定义函数的方式:
var abs = function (x) { if (x >= 0) { return x; } else { return -x; } };
这两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个 ; 表示赋值语句结束。
1.2 调用函数
调用函数时,按顺序传入参数即可:
由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
abs1(10, 'acb', 'xiaoming'); 10
传入的参数比定义的少也没有问题:
abs1(); NaN
当函数要求传入参数,调用时没有传参,此时 abs1(x) 函数的参数 x 将收到 undefined,计算结果为 NaN
要避免收到 undefined,可以对参数进行检查:
arguments
这个关键字只能在当前函数内部使用,作用是获取函数调用者传入的所有参数。
利用arguments
,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:
rest参数
由于JavaScript函数允许接收任意个参数,于是我们就不得不用arguments
来获取所有参数:
function foo(a, b) { var i, rest = []; if (arguments.length > 2) { for (i=2; i < arguments.length; i++) { rest.push(arguments[i]); } } console.log('a = ' +a); console.log('b = ' +b); console.log(rest); }
这种写法,逻辑没错,但是略显繁琐,在 ES6 标准引入了 rest 参数,上面的参数可以改写为:
function foo(a, b, ...rest) { console.log('a:', a); console.log('b:', b); console.log('rest:', rest) }
注意 rest 在函数参数部分的写法:(a, b ...rest) 这样就能直接获取除了 a, b 以外的所有参数。
当除了 a, b 参数以外,没有 rest 参数,查看下返回结果:
测试-1:
用rest参数编写一个sum()
函数,接收任意个参数并返回它们的和。
function foo(...rest) { var j=0; for (var i of rest) { // console.log(i); j += i; } return j; }
测试-2:
定义一个计算圆面积的函数area_of_circle()
,它有两个参数:
- r: 表示圆的半径;
- pi: 表示π的值,如果不传,则默认3.14
function area_of_circle(r, pi) { return pi?pi:3.14 * r **2; // if (pi === undefined) { // pi = 3.14; // } // return pi * r**2; }
变量作用域和解构赋值
如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量:
在来看一个函数内部的嵌套函数,判断变量的使用:
这说明JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。
变量提升
JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:
在 JavaScript 中, 函数调用的时候需要做两步:
1. 分析(AO对象)
(1) 先分析有没有参数
(2) 查看有没有局部变量
(3) 查看有没有声明函数
例 - 1:
例 - 2
(1)首先进行词法分析:
(2)执行函数
执行函数时,不会对内部子函数进行赋值,直接跳过。
由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var
申明函数内部用到的所有变量:
function foo() { var x = 1, // x初始化为1 y = x + 1, // y初始化为2 z, i; // z和i为undefined // 其他语句: for (i=0; i<100; i++) { ... } }
全局作用域
不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window
,全局作用域的变量实际上被绑定到window
的一个属性:
因此,直接访问全局变量course
和访问window.course
是完全一样的。
名字空间
全局变量会绑定到window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
// 唯一的全局变量MYAPP: var MYAPP = {}; // 其他变量: MYAPP.name = 'myapp'; MYAPP.version = 1.0; // 其他函数: MYAPP.foo = function () { return 'foo'; };
把自己的代码全部放入唯一的名字空间MYAPP
中,会大大减少全局变量冲突的可能。
局部作用域
为了解决块级作用域,ES6中引入了新的关键字 let,用 let 代替 var 可以申明一个块级作用域的变量:
解构赋值
从ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值。
什么是解构赋值?我们先看看传统的做法,如何把一个数组的元素分别赋值给几个变量:
var array = ['hello', 'JavaScript', 'ES6']; var x = array[0]; var y = array[1]; var z = array[2];
现在,在ES6中,可以使用解构赋值,直接对多个变量同时赋值:
如果数组本身还有嵌套,也可以通过下面的形式进行解构赋值,注意嵌套层次和位置要保持一致:
解构赋值还可以忽略某些元素:
如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:
var person = { name: '小明', age: 20, gender: 'male', passport: 'G-12345678', school: 'No.4 middle school' }; var {name, age, passport} = person; console.log(name); // 小明 console.log(age); // 20 console.log(passport); // G-12345678
对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的:
var person = { name: '小明', age: 20, gender: 'male', passport: 'G-12345678', school: 'No.4 middle school', address: { city: 'Beijing', street: 'No.1 Road', zipcode: '100001' } }; var {name, address:{city, zip}} = person; console.log(name); // '小明' console.log(city); // 'Beijing' console.log(zip); // undefined, 因为属性名是zipcode而不是zip // 注意: address不是变量,而是为了让city和zip获得嵌套的address对象的属性: console.log(address); // Uncaught ReferenceError: address is not defined
使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为undefined
,这和引用一个不存在的属性获得undefined
是一致的。如果要使用的变量名和属性名不一致,可以用下面的语法获取:
var person = { name: '小明', age: 20, gender: 'male', passport: 'G-12345678', school: 'No.4 middle school' }; let {name, passport:id} = person; console.log(name); // 小明 console.log(id); // G-12345678 // 注意: passport不是变量,而是为了让变量id获得passport属性: console.log(passport); // Uncaught ReferenceError: passport is not defined
解构赋值还可以使用默认值,这样就避免了不存在的属性返回undefined
的问题:
var person = { name: '小明', age: 20, gender: 'male', passport: 'G-12345678' }; // 如果person对象没有single属性,默认赋值为true: var {name, single=true} = person; name; // '小明' single; // true
有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:
// 声明变量: var x, y; // 解构赋值: {x, y} = { name: '小明', x: 100, y: 200}; // 语法错误: Uncaught SyntaxError: Unexpected token =
这是因为JavaScript引擎把{
开头的语句当作了块处理,于是=
不再合法。解决方法是用小括号括起来:
({x, y} = { name: '小明', x: 100, y: 200});
解构赋值使用场景
解构赋值在很多时候可以大大简化代码。例如,交换两个变量x
和y
的值,可以这么写,不再需要临时变量:
var x=1, y=2; [x, y] = [y, x]
快速获取当前页面的域名和路径:
使用解构赋值可以减少代码量,但是,需要在支持ES6解构赋值特性的现代浏览器中才能正常运行。目前支持解构赋值的浏览器包括Chrome,Firefox,Edge等。
2. 方法
在一个对象中绑定函数,称为这个对象的方法。
在 javascript 中,对象的定义是这样的:
var xiaoming = { name: '小明', birth: 1990 };
可以给对象 xiaoming 绑定一个函数,比如写一个 age 方法,返回 xiaoming 的年龄:
var xiaoming = { name: '小明', brith: 1990, age: function () { var y = new Date().getFullYear(); return y - this.brith } }; console.log(xiaoming.age()); // 29
绑定到对象上的函数称为方法,和普通函数也没啥区别,这里使用了关键字:this
在一个方法内部,this
是一个特殊变量,它始终指向当前对象,也就是xiaoming
这个变量。所以,this.birth
可以拿到xiaoming
的birth
属性。
如果分开来写,this就失效了。
function getAge() { var y = new Date().getFullYear(); return y - this.birth; } var xiaoming = { name: '小明', birth: 1990, age: getAge }; console.log(xiaoming.age()); // 29 console.log(getAge()); // NaN
注意这里 this的使用:
如果以对象的方法形式调用,比如 xiaoming.age(),该函数的 this 指向被调用的对象,也就是 xiaoming,这是符合我们预期的;
如果单独调用函数,比如 getAge(),此时,该函数的 this 指向全局对象,也就是 window
var fn = xiaoming.age; // 先拿到xiaoming的age函数 fn(); // NaN
这样是也不行的!要保证 this 指向正确,必须用 obj.xxx() 的形式调用!
apply
要指定函数的this
指向哪个对象,可以用函数本身的apply
方法,它接收两个参数,第一个参数就是需要绑定的this
变量,第二个参数是Array
,表示函数本身的参数。
用apply
修复getAge()
调用:
function getAge() { var y = new Date().getFullYear(); return y - this.birth; } var xiaoming = { name: '小明', birth: 1990, age: getAge }; console.log(xiaoming.age()); // 29 console.log(getAge.apply(xiaoming, [])); // 29 this指向xiaoming, 函数参数为空
另一个与 apply() 类似的方法是 call() ,唯一的区别是:
apply() 把参数打包成 Array 再传入;
call() 把参数按顺序传入.
比如调用Math.max(3, 5, 4)
,分别用apply()
和call()
实现如下:
Math.max.apply(null, [3, 5, 4]); // 5 Math.max.call(null, 3, 5, 4); // 5
对普通函数调用,我们通常把this
绑定为null
。
3. 高阶函数
JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
function f(x, y, f) { return f(x) + f(y); }
当我们调用add(-5, 6, Math.abs)
时,参数x
,y
和f
分别接收-5
,6
和函数Math.abs
,根据函数定义,我们可以推导计算过程为:
x = -5; y = 6; f = Math.abs; f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11; return 11;
3.1 高阶函数:map / reduce
(1)map()
map() 方法定义在 javascript 的 Array中,我们调用 Array的Map() 方法,传入自己的函数,就得到一个新的 Array作为结果:
map() 作为高阶函数,事实上它把运算规则抽象了,因此,可以使用map来做比较复杂的操作,比如把 Array的所有数字转为字符串:
(2)reduce()
Array 的 reduce() 把一个函数作用在这个 Array 的,这个函数必须接收两个参数,reduce() 把结果继续和序列的下一个元素做累积计算,其效果就是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
例 -1 对一个 Array求和:
function sum(x, y) { return x+y; } console.log([1, 2, 3, 4, 5, 6].reduce(sum)); // 21
例 - 2 对一个 Array 求积:
function product(arr) { return arr.reduce(function (x, y) { return x *y; }) } console.log(product([1,2,3,4,5])); // 结果 120
例 - 3 把[1, 3, 5, 7, 9]
变换成整数13579
var arr = [1, 3, 5, 7, 9].reduce(function (x, y) { return x*10+y }); console.log(arr); // 结果: 13579
例 - 4 不要使用JavaScript内置的parseInt()
函数,利用map和reduce操作实现一个string2int()
函数:
function string2int(s) { return s.split('').map(function (x) { return +x; }).reduce(function (x, y) { return x*10+y; }) } console.log(string2int('13579')); // 结果: 13579
例 - 5 请把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT']
,输出:['Adam', 'Lisa', 'Bart']
。
function normalize(arr) { return arr.map(function (x) { return x[0].toUpperCase() + x.slice(1).toLowerCase(); }) } console.log(normalize(['adam', 'LISA', 'barT'])); // ["Adam", "Lisa", "Bart"]
例 - 6 Array 字符串 --> 数字 Array 数字 --> 字符串
// Array 元素字符串 转 数字 var arr = ['1', '2', '3']; var r; r = arr.map(Number); console.log(r); // [1, 2, 3] // Array 元素 数字 转 字符串 s = r.map(String); console.log(s); // ["1", "2", "3"]
3.2 高阶函数: filter
filter也是一个常用的操作,它用于把Array
的某些元素过滤掉,然后返回剩下的元素。
和map()
类似,Array
的filter()
也接收一个函数。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是true
还是false
决定保留还是丢弃该元素。
例如,在一个 Array 中,删掉偶数,只保留奇数,可以这么写:
var arr = [1, 2, 4, 5, 6, 9, 10, 15]; var r = arr.filter(function (x) { return x % 2 !== 0 // 过滤条件,条件为true保留,为false剔除 }); console.log(r); // [1, 5, 9, 15]
把一个Array
中的空字符串删掉,可以这么写:
var arr = ['A', '', 'B', null, undefined, 'C', ' ']; var r = arr.filter(function (s) { return s && s.trim(); // 注意:IE9以下的版本没有trim()方法 }); r; // ['A', 'B', 'C']
可见用filter()
这个高阶函数,关键在于正确实现一个“筛选”函数。
利用 filter,可以去除 Array 的重复元素:
var r, arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry']; r = arr.filter(function (element, index, self) { return self.indexOf(element) === index; }); console.log(r); // ["apple", "strawberry", "banana", "pear", "orange"]
去除重复元素依靠的是indexOf
总是返回第一个元素的位置,后续的重复元素位置与indexOf
返回的位置不相等,因此被filter
滤掉了。
例 - 1 请尝试用filter()
筛选出素数:
function get_primes(arr) { return arr.filter( x => { let result = true; let end =Math.sqrt(x); // 开平方开出来是整数的要剔除掉 let flag = 0; for (let i = 2; i <= end; i++) { // 能被大于2且小于本身的整数整除的不是素数 if (x % i === 0) { flag = 1; break; } } if (x === 1) { result = false; } else if ( flag === 0) { result = true; } else { result = false; } return result; })} var x, r, arr = []; for (x = 1; x < 100; x++) { arr.push(x); } r = get_primes(arr); if (r.toString() === [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97].toString()) { console.log('测试通过!'); } else { console.log('测试失败: ' + r.toString()); }
3.3 高阶函数:sort()
JavaScript的Array
的sort()
方法就是用于排序的,但是排序结果可能让你大吃一惊:
// 看上去正常的结果: ['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft']; // apple排在了最后: ['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple'] // 无法理解的结果: [10, 20, 1, 2].sort(); // [1, 10, 2, 20]
第二个排序把apple
排在了最后,是因为字符串根据ASCII码进行排序,而小写字母a
的ASCII码在大写字母之后。
第三个排序结果是什么鬼?简单的数字排序都能错?
这是因为Array
的sort()
方法默认把所有元素先转换为String再排序,结果'10'
排在了'2'
的前面,因为字符'1'
比字符'2'
的ASCII码小。
sort()
方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。
比较数字类型:
要按数字大小排序,我们可以这么写:
var arr = [10, 20, 1, 2]; arr.sort(function (x, y) { if (x> y) { return 1; } if (x< y) { return -1; } return 0; }); console.log(arr); // 结果: [1, 2, 10, 20]
从大到小排序:
var arr = [10, 20, 1, 2]; arr.sort(function (x, y) { if (x> y) { return -1; } if (x< y) { return 1; } return 0; }); console.log(arr); // 结果: [20, 10, 2, 1]
比较字母类型:
var arr = ['Google', 'apple', 'Microsoft']; arr.sort(function (s1, s2) { x1 = s1.toLowerCase(); x2 = s2.toLowerCase(); if (x1 < x2) { return -1; } if (x1 > x2) { return 1; } return 0; }); console.log(arr); // 结果:["apple", "Google", "Microsoft"]
注意:sort() 方法会直接对 Array 进行修改,它返回的结果仍然是当前 Array
Array 其他高阶函数介绍
对于数组,除了map()
、reduce
、filter()
、sort()
这些方法可以传入一个函数外,Array
对象还提供了很多非常实用的高阶函数。
every
every()
方法可以判断数组的所有元素是否满足测试条件。
例如,给定一个包含若干字符串的数组,判断所有字符串是否满足指定的测试条件:
var arr = ['Apple', 'pear', 'orange']; console.log(arr.every(function (s) { return s.length > 0; // 因为每个元素长度都大于0,所以返回 true })); console.log(arr.every(function (s) { return s.toLowerCase() === s; // 原元素不是每个都是小写,所以返回false }));
注意:every 高阶函数,针对 Array 中每个元素进行检查,如果有一个不满足条件则返回 false。
find
find()
方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined
:
var arr = ['Apple', 'pear', 'orange']; console.log(arr.find(function (s) { return s.toLowerCase() === s; // pear 返回符合条件的第一个元素 })); console.log(arr.find(function (s) { return s.toUpperCase() === s; // undefined 因为没有任何一个元素满足条件,则返回 undefined }));
findIndex
findIndex()
和find()
类似,也是查找符合条件的第一个元素,不同之处在于findIndex()
会返回这个元素的索引,如果没有找到,返回-1
:
var arr = ['Apple', 'pear', 'orange']; console.log(arr.findIndex(function (s) { return s.toLowerCase() === s; // 返回第一个符合条件元素的索引,没有则返回 -1 })); console.log(arr.findIndex(function (s) { return s.toUpperCase() === s; // 返回第一个符合条件元素的索引,没有则返回 -1 }));
3.4 闭包
函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
function lazy_sum(arr) { var sum = function () { return arr.reduce(function (x, y) { return x+y; }) }; return sum; } var f = lazy_sum([1,2,3,4,5]); console.log(f); // 返回 function () { ... } console.log(f()); // 返回结果: 15
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
var f1 = lazy_sum([1, 2, 3, 4, 5]); var f2 = lazy_sum([1, 2, 3, 4, 5]); f1 === f2; // false
f1()
和f2()
的调用结果互不影响。
闭包
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量
3.5 箭头函数
ES6标准新增了一种新的函数:Arrow Function(箭头函数)。
具体写法:
var fn = x => x * x; console.log(fn(10)); // 100
x => x * x;
等价于
function (x) { return x * x}
如果参数不是一个,就需要用括号()括起来:
// 两个参数: (x, y) => x * x + y * y // 无参数: () => 3.14 // 可变参数: (x, y, ...rest) => { var i, sum = x + y; for (i=0; i<rest.length; i++) { sum += rest[i]; } return sum; }
练习 1:
请使用箭头函数简化排序时传入的函数:
var arr = [10, 20, 1, 2]; arr.sort((x, y) => { return (x>y) ? 1:-1; // 在做 if 判断时,首先考虑能否使用 三元运算 }); console.log(arr); // [1, 2, 10, 20]
附加一道面试题:
arr = [1, 2, 2, 3, 4, 5, 4, 5] 去重并改变原有顺序
方法1:
通过索引判断
arr = [1, 2, 2, 3, 4, 5, 4, 5]; function uniq(arr) { var temp = []; for (var i=0; i<arr.length; i++) { if (temp.indexOf(arr[i]) === -1) { // 当值存在则返回索引位置,不存在则返回 -1 temp.push(arr[i]) // 当元素不存在则添加元素 } } return temp; } console.log(uniq(arr));
方法2:
通过 Set 类型直接去重
arr = [1, 2, 2, 3, 4, 5, 4, 5]; console.log([... new Set(arr)]); // '...' 是 js中的扩展运算符, 这里是将 set 类型遍历转换为 数组类型