JavaScript-函数
函数基础
定义
1 |
|
funciton
: 定义函数的关键字abs
: 函数名(x)
: 函数参数{...}
: 函数体
还有一种定义方式
1 |
|
function (x) {...}
: 是一个匿名函数abs
: 这个匿名函数赋值给了变量abs,所以通过abs
就能调用该函数
调用
1 |
|
为了避免收到undefined,可以对参数检查
1 |
|
arguments
arguments
是js的一个关键字,只能在函数内部起作用,永远指向当前函数的调用者传入的所有参数
.类似array
但并不是array
.
1 |
|
利用arguments
就可以获取传入的所有参数,所以即使不定义参数,也能获取传入的参数.
1 |
|
实际上arguments
最常用于判断传入参数的个数
1 |
|
rest
假如你的函数只定义了2个参数,但我们要使用除了前两个已定义参数外的所有参数
,此时是使用arguments
就会显得很麻烦.ES6后引入了...rest
关键字来获取不定参数
1 |
|
...rest
只能写在最后- 如果传入的参数连已定义的参数都没有填满,rest会接收一个空数组(非undefined)
return不要分行
因为JS引擎会自动加;
,如果return分行写的话容易变成这样
1 |
|
那么返回值就会出错.正确的写法是:
1 |
|
变量作用域
函数作用域
如果一个变量在函数体内申明,该变量的作用域为整个函数体,函数体外不可引用该变量
不同函数内部的同名变量互相独立,互不影响
JS的函数可以嵌套,内部函数可以访问外部函数定义的变量,反之则不行
JS的函数在查找变量时从自身函数定义开始,如果内部函数定义了与外部函数重名的变量,则以内部变量的定义为准
JS会扫描整个函数体的语句,把所有申明的变量
提升
到函数顶部1
2
3
4
5
6
7
8
9
10function foo() {
var x = 'Hello, ' + y; // 这里不会报错,因为JS会自动提升变量y的申明
console.log(x);
var y = 'Bob'; // 但是它只提升申明,不提升赋值,所以最终输出会是undefined
}
foo();
/*
输出如下:
Hello, undefined.
*/
因此,我们在函数内部定义变量,必须严格遵守在函数内部首先申明所有变量
这一原则.最常见的做法是用一个var
申明所有用到的变量.
1 |
|
ES6后,建议使用
let
代替var
.
全局作用域
不在任何函数内定义的变量就具有全局作用域
.JS默认有一个全局对象window
,全局作用域的变量都会被绑定成window
的一个属性
.
1 |
|
同理,函数的另一种定义方式,把匿名函数赋值给变量,这个变量实际上也是一个全局变量.因此,顶层函数的定义也被视为一个全局变量,绑定到window
的一个属性.
1 |
|
其实不难想到,每次调用的alert()
函数其实也是window
的一个变量
1 |
|
这说明JS只有一个全局作用域
,任何变量(函数也被视为变量),如果在当前函数作用域没有找到,就会继续往上查找,最后如果在全局作用域中也没有,就会报ReferenceError
.
名字空间
所有全局变量都会绑定到window
上,不同的JS文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,就会造成冲突,而且难以发现.一个解决办法就是,把自己的所有变量和函数全部绑定到一个全局变量中.这个全局变量也被称为名字空间
.
1 |
|
很多著名的JS库都是这样做: JQuery, YUI, underscore等
块级作用域
JS的变量作用域实际上是函数内部,我们在for
循环等语句块中无法定义具有局部作用域的变量.
因此,ES6引入了let
关键字,可以用来申明一个块级作用域的变量.
1 |
|
常量
ES6前,通常用一个全大写的变量来表示”这是一个常量,不要修改它的值”
1 |
|
ES6后,引入const
来定义常量,与let
一样都具有块级作用域
1 |
|
const, let, var
首先const
用来定义常量,它不可变,具有块级作用域.
需要特别区分的是let
和var
区别点 | let | var |
---|---|---|
作用域 | 块级 | 函数 |
变量提升 | 不会提升(先申明再使用) | 提升申明 |
全局对象属性 | 不会加到window 的属性 | 会加到window 的属性 |
重复声明 | 不允许,会报错 | 允许,值可变 |
总结:
现在JS开发中,推荐使用let
和const
来替代var
.这样可以避免一些常见的作用域相关问题,使代码更可预测和更容易维护.
某些特殊情况需要用到var
:
- 老旧浏览器兼容性问题
- 特殊的跨作用域访问场景
- 特定的闭包使用场景
解构赋值
一次过赋值多个变量
1 |
|
嵌套的话注意位置保持一致
1 |
|
忽略元素赋值
1 |
|
从一个对象中提取多个属性
1 |
|
同样,对于嵌套的对象属性也可以
1 |
|
使用场景
解构赋值可以大大简化代码.
1 |
|
方法
this
在一个对象
中绑定一个函数
,该函数即为该对象的方法
.
1 |
|
this
类似python的self
,是一个特殊变量,始终指向当前对象,也就是xiaoming
.然而this
有一些地方需要注意:
只有通过
obj.function()
方式调用,this
才能正确指向对象其他调用会直接指向全局对象
window
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function getAge() {
let y = new Date().getFullYear();
return y - this.birth;
}
let xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常结果
getAge(); // 直接调用,this指向window,自然没有birth这个属性,返回NaN
// 这样写也是不行的
let fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaNstrict
模式下,错误的调用会让this
指向undefined
1
2
3
4
5
6
7
8
9
10
11
12
13'use strict';
let xiaoming = {
name: '小明',
birth: 1990,
age: function () {
let y = new Date().getFullYear();
return y - this.birth;
}
};
let fn = xiaoming.age;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined内嵌函数内的调用也会指向全局对象,可以先用
that
关键字捕获1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16'use strict';
let xiaoming = {
name: '小明',
birth: 1990,
age: function () {
let that = this; // 在方法内部一开始就捕获this
function getAgeFromBirth() {
let y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
apply
在独立函数调用中的this
:
- 使用
strict
,this
指向undefined
- 不用
strict
,this
指向window
的属性
如果要控制this
的指向,规定它指向哪个对象,可以使用函数本身的apply
方法.
1 |
|
另一个与apply()
类似的方法是call()
,区别在于:
apply()
把参数打包成Array
再传入call()
把参数按顺序传入
1 |
|
对于普通函数的调用,我们通常把
this
绑定到null
.
普通函数
是指不依赖任何对象上下文的函数,特别像Math.max()
这样的工具函数.与之相对的就是对象的方法
.
普通函数
根本不会使用this
,传入null只是一种约定俗成的做法,表明我们不关心this
的值.
装饰器
1 |
|
其实这里就相当于重写parseInt
,添加计数器功能,然后通过apply
,保留原函数的功能.这样每次调用该函数时都会自动计数.
这就是JS典型的装饰器(Decorator)模式
.
这种模式一般用于:
- 函数调用统计
- 性能监控
- 日志记录
- 调试
高阶函数
higher-order function
: 一个函数以另一个函数作为参数,就称为高阶函数
.
JS 的函数都指向某个变量,变量可以指向函数,函数的参数可以接收变量,所以函数可以作为另一个函数的参数.
1 |
|
map
比如我们有一个函数f(x)=x*x
,要对一个数组上的所有元素都使用这个函数,此时就可以使用map
1 |
|
JS的map()
定义在Array
中,所以我们调用Array
的map()
,传入自己的函数,就得到一个新得Array
返回.
1 |
|
当然,我们也可以写个循环实现上面的目的.但是map()
作为高阶函数,事实上它把运算规则抽象了,所以我们可以根据传入的函数,实现各种复杂的操作,代码还能保持简单明了.
1 |
|
reduce
reduce
同样也是作用在array
的每个元素上,但是它是累积
的.
比如通过reduce
把函数f(x)
作用在[x1, x2, x3, x4]
上,其效果就是
1 |
|
1 |
|
如果数组元素只有一个,就要提供一个额外的初始参数
1 |
|
把[1, 3, 5, 7, 9]
变成一个整数
1 |
|
不使用parseInt()
,利用map
和reduce
,把'13579'
转为数字
.
1 |
|
filter
filter
用于把array
的某些元素过滤掉,返回剩下的.
filter
把传入的函数
依次作用到array
每个元素,根据返回值true/false
来决定保留还是丢弃.
1 |
|
回调函数
callback function
是一个编程概念.作为参数传递给另一个函数的函数,则称为回调函数
.当某个事件发生或特定条件满足时,这个被传递的函数就会被”调用回来”执行.
filter
接收的回调函数其实可以由多个参数:
- 第一个: 表示
array
的某个元素 - 第二个: 表示元素的位置
- 第三个: 数组本身
1 |
|
用filter
去除重复元素
1 |
|
sort
排序的核心是比较两个元素的大小,但是如果元素是字符串,在数学上则无法比较.因此比较的过程必须抽象出来.通常规定:
x<y
返回-1
x=y
返回0x>y
返回1排序算法不关心具体的比较过程,只根据结果排序.
JS 的array
有个sort()
方法,然而有点坑爹.
1 |
|
第二个排序apple
排在最后,那时因为小写字母a
的ASCII码在大写字母之后.
第三个则是因为,sort()
默认把所有元素先转换成String
再排序,结果'10'
排在'2'
前面,因为'1'
的ASCII码比'2'
小.
还好的是sort()
是一个高阶函数,我们可以自己实现比较函数传进去比较.
1 |
|
如果要实现倒序排序,可以这样
1 |
|
y-x
:
- 如果
x<y
,则返回正数- 如果
x>y
,则返回负数- 如果
x=y
,则返回0天才
默认情况下对字符串排序是通过比较ASCII码来实现,那如果想忽略大小写来排序呢?也不难,把元素全部转为大写/小写再比较就行了.
1 |
|
最后从上面不难看出,sort()
是会直接修改原array
的.这个要注意一下.
1 |
|
array的其他高阶函数
every
用于判断数组的所有元素
是否满足测试条件
1 |
|
find
用于查找符合条件的第一个
元素,找到就返回该元素,否则就返回undefined
.
1 |
|
findIndex
与find
类似,也是找符合条件的第一个
元素,但是它返回的是index,没有找到则返回-1
1 |
|
forEach
这个跟map()
类似,也是把每个元素依次作用与传入的函数,但不会返回新数组.
forEach
常用于遍历数组,传入的函数不需要返回值.
1 |
|
返回函数
高阶函数
除了可以接收函数作为参数,还可以返回函数
.
比如我们要实现一个对array
的求和函数,但是我们不需要立即求和,而是在后面的代码中根据需要再计算:
1 |
|
闭包
闭包
是JS的一个重要特性,指的是函数能够记住并访问它的词法作用域,即使当这个函数在其原始作用域之外执行时也能正常工作.
1 |
|
上面的例子说明,即使变量是定义在函数内部
的,但是通过return
返回的函数依旧可以正常访问/修改
私有变量.因为返回函数形成了闭包
.
但有一点要注意,返回的函数
并不会在返回时执行
,而是在实际调用的才会执行
.
闭包陷阱
1 |
|
要避免这种陷阱,你可以:
使用
let
代替var
,let
作用域决定了每次循环都会绑定新的i
.(recommend)创建
立即执行函数
,这是JS的一个语法,用于创建一个匿名函数并立刻执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function count() {
let arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
let [f1, f2, f3] = count();
f1(); // 1
f2(); // 4
f3(); // 9这里再创建了一个函数,用该函数的参数绑定循环变量的当前值,无论该循环后续如何更改,已绑定到函数参数的值不变.
可以看出,里面嵌套很复杂,最好还是不要用这种,这里更多是为了引出
创建一个匿名函数并立刻执行
的语法:1
2
3
4
5
6
7
8// 理论上讲,创建一个匿名函数并立刻执行可以这么写
function (x) {return x * x} (3);
// 但是JS语法解析的问题,需要用括号把函数括起来,不然会报SyntaxError
(function (x) {return x * x}) (3);
// 通常会把它拆开写,好看点
(function (x) {
return x * x;
}) (3);不要在返回函数中引用循环变量.(recommend)
私有变量
JS的闭包除了返回一个函数,延迟执行外,还有一个最强大的功能,就是在对象内部封装一个私有变量
.
想java和c++,可以用private
,但JS没有class
只有函数,需要借助闭包,封装私有变量.
1 |
|
在返回的对象中,实现了一个闭包,闭包携带了局部变量x
,并且这个x
无法被访问.换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来.
创建新函数
举个例子: 闭包还可以把多参数的函数变成单参数的函数.比如要计算x^y
可以用Math.pow(x, y)
函数,不过考虑到经常计算x^2或x^3,可以利用闭包创建新的函数pow2
或pow3
.
1 |
|
这种做法也被称为函数工厂
,这种函数也被称为工厂函数
.
箭头函数
ES6新增一种新的函数: 箭头函数
,因为它的定义就是一个箭头=>
1 |
|
箭头函数相当于匿名函数
,并且简化了函数定义.它有两种定义格式:
像上面的,只包含一个表达式,
{...}
和return
都省略包含多种语句的,不能省略
{...}
和return
1
2
3
4
5
6
7x => {
if (x > 0) {
return x * x;
} else {
return - x * x;
}
}
如果参数不是只有一个,就要用()
括起来
1 |
|
如果返回一个对象
1 |
|
this修复
箭头函数没有自己的 this
绑定。箭头函数会捕获其被定义时的上下文的 this
值,而且这个绑定是永久的,不能被改变。
词法作用域
(Lexical scope):是指变量的作用域在代码编写时(词法分析时)就已经确定,而不是在运行时确定.简单来说,就是变量的访问权限由变量定义的位置决定.
1 |
|
再来两个例子
1 |
|
第一个例子的箭头函数直接在对象字面量
中定义,所以继承了全局作用域的this
对象字面量: 直接使用
{}
创建对象的语法
第二个例子的箭头函数在普通函数getAge
内定义,继承外部作用域,也就是getAge
的this
1 |
|
标签函数
对于模板字符串
,除了方便引用变量构造字符串外,还有一个更强大的功能,就是使用标签函数
(tag function)
1 |
|
1 |
|
模板字符串以特定标签函数名开头,就是标签函数的调用方式.
上面就是调用标签函数sql()
.
标签函数接收两个参数:
将模板字符串的
字符串部分,转换成数组
,赋值给第一个参数string
,上面的例子就是:1
["SELECT * FROM users WHERE email=", " AND password=", ""]
将模板字符串的
变量部分,转换成数组
,解析后传给不定参数...exps
,也就是:1
["test@example.com", "hello123"]
先在内部把
strings
转换成SQL字符串,然后把...exps
作为参数,就可以实现一个安全的SQL查询.例如这样1
2
3
4
5
6
7
8
9
10
11
12function update() {
let sql = strings.join('?');
// 执行数据库更新
// TODO:
}
// 调用非常简洁
let id = 123;
let age = 21;
let score = 'A';
update`UPDATE users SET age=${age}, score=${score} WHERE id=${id}`;
生成器
ES6引入的新数据类型,一个生成器看上去像一个函数,但可以返回多次
.定义JS生成器标准的哥们借鉴了python生成器的概念和语法.
函数在执行过程中如果没有遇到return
,控制权无法交回被调用的代码.(函数如果没有显式声明return
,默认都会有一个return undefined
).生成器与函数类似,定义如下:
1 |
|
function*
,定义时比函数多一个*
- 除了
return
,还可以用yield
返回多次
举个例子, 斐波那契数列:
1 |
|
1 |
|
那么跟python类似的,当数据量无限增大,这个函数在内存的占用也会无限增大,所以使用生成器
是个更好的方式
1 |
|
生成器可以看成一个可以记住执行状态的函数
.
需要注意的是生成器执行到yeild
就会结束,要让它保持生成多个元素,一般都需要加个循环
包裹.