函数的定义
函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。
函数的本质
函数的本质是一种特殊的对象
函数的声明
- 方法一:
Function Expression函数表达式。这种写法将一个anonymous function匿名函数赋值给变量a,左边是赋值,右边是函数表达式。
let a = function(x,y){
return x + y
}
采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。下面代码在函数表达式中,加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错。
let print = function x(){
console.log(typeof x);
};
x
// ReferenceError: x is not defined
print()
// function
- 方法二:
function命令。使用function命令来新建一个函数,带有函数名,也叫做named function具名函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。
function 函数名(parameters形式参数1,parameters形式参数2){
语句
return 返回值
}
- 方法三:
Function构造函数。这种声明函数的方式非常不直观,几乎无人使用,但是能让你知道函数是谁构造的,所有的函数都是function构造的,包括Object,Array,Function。下面代码中,Function构造函数接受三个参数,除了最后一个参数是add函数的“函数体”,其他参数都是add函数的参数。
let add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
- 方法四:
Arrow Function箭头函数- 方式
1:其中x为输入的参数,x*x为函数体。箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像下面的,只包含一个表达式,连{ ... }和return都省略掉了。例如,let f1 = x => x*x - 方式
2:如果参数不是一个,就需要用括号()括起来, 例如let f2 = (x,y) => x+y - 方式
3:还有一种可以包含多条语句,这时候就需要使用{ ... }和return。注意,如果加了{...}就不能省略return,因为JS会判断不出哪个语句需要被返回 例如,let f3 = (x,y) => {return x + y} - 方式
4:如果要返回一个对象,就要注意,如果是单个表达式,会出错因为和函数体的{ ... }或者说block块级区域 代码的语法有冲突,所以要加一个()包裹起来例如,let f4 = (x,y) =>({name:x, age:y})
- 方式
函数的重复声明
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。 下面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的
function f() {
console.log(1);
}
f() // 2
function f() {
console.log(2);
}
f() // 2
函数本身vs函数调用
fn函数本身
下面的代码不会有任何结果,因为函数fn只是被定义,还未被调用
let fn = () => console.log('hi');
fn
fn()函数调用
下面的代码会被执行,因为()才是调用函数
let fn = () => console.log('hi')
fn()
进阶知识
变量fn保存了匿名函数的地址,然后这个地址被复制到了fn2,fn2()调用了匿名函数,真正的函数既不是fn,也不是fn2,他们都只是匿名函数的引用而已
let fn = () => console.log('hi');
let fn2 = fn;
fn2()
函数调用时机
实例1
问题: 请问打印出多少?
let a = 1;
function fn(){
console.log(a)
}
答案:不知道,因为没有调用代码
实例2
问题: 请问打印出多少?
let a = 1;
function fn(){
console.log(a)
}
fn()
答案: 打印出1,因为调用了函数fn
实例3
问题: 请问打印出多少?
let a = 1;
function fn(){
console.log(a)
}
a = 2
fn()
答案: 打印出2,因为fn()在a = 2之后才被执行
实例4
问题: 请问打印出多少?
let a = 1;
function fn(){
console.log(a)
}
fn()
a = 2
答案: 打印出1,因为fn()在a = 2之前被执行
实例5
问题: 请问打印出多少?
let a = 1;
function fn(){
setTimeout(()=>{
console.log(a)
},0)
}
fn()
a = 2
答案: 打印出2,因为setTimout()在a = 2 完成之后才执行
实例6:for循环中诡异的SetTimout
问题: 请问打印出多少?
let i = 1;
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
答案: 6个6,因为for循环的时候,每次执行一次i,触发setTimout()函数,等for循环结束了,i为6的时候,6个setTimeout()才最终执行
实例7:let解决诡异的SetTimeout
问题: 请问打印出多少?
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
答案: 0,1,2,3,4,5 因为for循环体中加入setTimeout定时器,for语句每次循环时,定时器都处于等待状态,for循环执行一次,定时器就得到一次机会,直到循环执行完毕,定时器才开始执行,循环几次定时器就执行几次,而let只能声明一次,所以每次都是新的变量,也可以理解为每次循环都被被复制了一个i,加上循环的i,一共有6个i。
原理分析
- 规则:同步优先、异步靠边、回调垫底。
- js的执行机制:
js是单线程环境,从上到下、依次执行,即 同步执行;在实例6中,for循环是同步代码,setTimeout是异步代码。js在执行代码的过程中,碰到同步代码会依次执行,碰到异步代码就会将其放入任务队列中进行等待,当同步代码执行完毕后再开始执行异步代码,即 异步执行。 - js作用域问题:当同步代码执行完毕后,开始执行异步的
setTimeout代码,执行setTimeout时需要从当前作用域内寻找一个变量i,此时for循环已执行完毕,当前i=6,所以执行setTimeout时输出为6,任务队列中的剩余5个setTimeout也依次执行,输出为6。 - 实例
6中的let在for循环的外部。而在实例7中,let只在代码块内才有效,let只能声明一次。变量i是用let声明的,当前的i只在本轮循环中有效,每次循环的i其实都是一个新的变量,所以setTimeout定时器里面的i,其实是不同的变量,即最后输出0-5。(若每次循环的变量i都是重新声明的,如何知道前一个循环的值?这是因为JavaScript引擎内部会记住前一个循环值)
实例8:实例7的let的替代解决方法
- 方法1-闭包。通过闭包,将
i的变量驻留在内存中,当输出j的时候,引用的是外部函数的i,所以执行setTimout时已经确定了里面的输出了
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})(i);
}
- 方法2-拆分
setTimeout结构。将setTimeout的定义和调用分别放到不同部分
function timer(i) {
setTimeout( console.log( i ), i*1000 );
}
for (var i=1; i<=5;i++) {
timer(i);
}
- 方法3-设置
setTimeout第三个参数。由于每次传入的参数是从for循环里面取到的值,所以会依次输出1到5
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000, i );
}
scope作用域
定义
scope作用域指的是变量存在的范围,每个函数都会默认创建一个作用域
分类
JavaScript有三种作用域:
- 全局作用域,变量在整个程序中一直存在,所有地方都可以读取
- 函数作用域,变量只在函数内部存在了
- 块级block作用域, 和
let搭配,外层作用域无法获取到内层作用域,非常安全明了。即使外层和内层都使用相同变量名,也都互不干扰
全局变量vs局部变量
- 在全局作用域,声明的是全局变量, 例如
window的属性就是全局变量 - 其他的都为局部变量
实例1
function fn(){
let a = 1
}
fn()
console.log(a) // 报错,因为全局变量a不存在,函数fn 访问不到局部变量a
原因: 因为变量a只存在作用域中,外部访问不到
作用域的规则
- 函数可以嵌套,作用域也可以嵌套
- 如果有多个作用域有同名变量
a,那么查找a的声明时,就向上取最近的作用域,简称'就近原则' - 查找
a的过程与函数执行无关,a的值与函数执行有关
实例2
function f1(){
let a = 1;
function f2(){
let a = 2
console.log(a)
}
console.log(a)
a = 3
f2()
}
f1()
// 1 这个a的值是f1()里面的a
// 2 这个a的值是f2()里面的a
实例3
function f1(){
let a = 1;
function f2(){
let a = 2
function f3(){
console.log(a)
}
a = 22
f3()
}
console.log(a)
a = 100
f2()
}
f1()
// 1 这个a的值是f1()里面的a
// 22 这个a的值是f3()执行f2里面的a
closure闭包
定义
函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。
原理
在实例3中,就存在了closure闭包, 如果一个函数用到了外部的变量,那在这里,外部变量a和f3函数就组成了闭包。
function f2(){
let a = 2
function f3(){
console.log(a)
}
}
作用
闭包常常用来间接访问一个变量。通过暴露一个函数,让别人可以间接访问这个变量。 另一个就是让这些变量的值始终保持在内存中,不会在外部函数被调用后被自动清除。
优缺点
优点:避免全局变量的污染;能够读取函数内部的变量;在内存中维护一个变量
缺点:
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在
IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。 - 闭包会在父函数外部,改变父函数内部变量的值。解决方法是, 不要随便改变父函数内部变量的值。
应用场景:实现封装;模拟面向对象,不同的对象(类的实例)拥有独立的成员及状态,互不干涉
parameters形式参数
定义
形式参数就是函数定义的时候规定的参数,简单的说,就是给参数取名字
原理
在下面的代码中,函数声明时候规定的参数x和y就是形式参数,并不是实际的参数。当输入实际参数1和2的时候,两个参数会被复制给x和y,然后x和y进行操作。
function add(x,y){
return x + y
}
add(1,2)
形式参数也可以认为是变量声明,下面的代码和上面的代码是等价的。JS 中,形式参数可多可少,不影响运行
function add(){
var x = arguments[0]
var y = arguments[1]
return x + y
}
返回值
定义
返回值指函数执行完毕后返回的值
原理
返回值的类型包括:Number数字 String字符串 Boolean布尔值 function函数 Ojbect对象, 默认返回 undefined,只有在函数执行了之后才会有返回值,没有执行,就没有返回值。在下面的例子中,函数没有写return,所以返回值是undefined。
function hi(){
console.log('hi')
}
hi() // undefined
在下面这个例子中,也是返回undefined, 因为console.log('hi')的值是undefined,console.log()方法只有打印值,没有返回值,返回的只有undefined
function hi(){
return console.log('hi')
}
hi() // undefined