JavaScript 函数(二)

237 阅读6分钟

函数的要素

每个函数都有以下要素:调用时机、作用域、闭包、形式参数、返回值、调用栈、函数提升、arguments(除了箭头函数)、this(除了箭头函数)

1. 调用时机

一个函数的调用时机不同,得到的结果就不同。

示例1

let a = 1
function fn() {
  console.log(a)
}
//不知道打印出多少,因为没有调用函数

示例2

let a = 1
function fn() {
  console.log(a)
}
fn()  //打印出1

示例3

let a = 1
function fn() {
  console.log(a)
}
a = 2
fn()  //打印出2

示例4

let a = 1
function fn() {
  console.log(a)
}
fn()
a = 2
//打印出1

示例5

let a = 1
function fn() {
  setTimeout(() => {
    console.log(a)
  }, 0)
}
fn()
a = 2

上面代码会打印出 2,因为 setTimeout 是异步执行的,要等 a = 2 执行完了再执行 fn

示例6

let i = 0
for (i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  }, 0)
}

上面代码会打印6个6,而不是0,1,2,3,4,5,由于 setTimeout 是异步执行的,所以 for 循环执行结束后(i = 6)再执行setTimeout打印出 i,且打印六次。

示例7

for (let i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  }, 0)
}

上面代码会打印出0,1,2,3,4,5,使用 let 声明的变量在块级作用域内能强制执行更新变量。

2. 作用域:就近原则 & 闭包

每个函数都会默认创建一个作用域。

function fn() {
  let a = 1
}
console.log(a)  //a 不存在
fn()
console.log(a) // a 还是不存在

上面代码中,就算 fn 执行了,也访问不到作用域里面的 aa 是局部变量。

关于全局变量和局部变量:

  • 在顶级作用域声明的变量是全局变量
  • window 的属性是全局变量
  • 其他都是局部变量

函数可以嵌套,作用域也可以嵌套。

function f1() {
  let a = 1

  function f2() {
    let a = 2
    console.log(a) //打印出2
  }

  console.log(a)   //打印出1
  a = 3
  f2()
}
f1() 
// 1
// 2

作用域规则:如果多个作用域有同名变量 a,那么查找 a 的声明时,就向上取最近的作用域,简称 “就近原则”,查找 a 的过程与函数执行无关,但 a 的值与函数执行有关。

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
// 22

如果一个函数用到了外部的变量,那么这个函数加这个变量,就叫做闭包,上面代码中的 let a = 2f3 组成了闭包。

3. 形式参数

形式参数指非实际参数。

function add(x, y) {
  return x + y
}
//其中 x 和 y 就是形参,因为并不是实际的参数

add(1,2)
// 调用 add 时,1 和 2 是实际参数,会被赋值给 x y

形式参数可以认为是变量声明,上面的代码近似等价于下面代码。

function add() {
  var x = arguments[0]
  var y = arguments[1]
  return x + y
}

形式参数可多可少,形参只是给参数取名字。

function add(x) {
  return x + arguments[1]
}
add(1,2)  //3

4. 返回值

  • 每个函数都有返回值
  • 函数执行完了后才会返回
  • 只有函数才有返回值
function hi() {
  console.log('hi')
}
hi()
//没写 return,所以返回值是 undefined

function hi() {
  return console.log('hi')
}
hi()
//返回值为 console.log('hi') 的值,即 undefined

5. 调用栈(Call stack)

JS 引擎在调用一个函数前,需要把函数所在的环境压(push)到一个数组里,这个数组叫做调用栈,等函数执行完了,就会把环境从调用栈里弹(pop)出来,然后 return 到之前的环境,继续执行后续代码。MDN Call stack(调用栈)

function f(n) {
  return n !== 1 ? n * f(n - 1) : 1
}
// 理解递归
// f(4)
// = 4 * f(3)
// = 4 * (3 * f(2))
// = 4 * (3 * (2 * f(1)))
// = 4 * (3 * (2 * (1)))
// = 4 * (3 * (2))
// = 4 * (6)
// 24

上面代码是一个有关于阶乘的递归函数,递归就是先递进(压栈),再回归(弹栈),递归函数的调用栈很长,调用栈的长度是有限的,如果调用栈中压入的帧过多,程序就会崩溃,这就是爆栈。

6. 函数提升

函数提升就是,不管把具名函数声明在哪里,它都会跑到第一行。

add(1, 2)
function add(x, y) {
  return x + y
}
//3

如果将一个匿名函数赋值给变量时,这时的匿名函数声明不会提升。

let fn = function() {}  //这是赋值,右边的匿名函数声明不会提升

注意,采用function命令和var赋值语句声明同一个函数时,由于存在函数提升,最后会采用var赋值语句的定义。

var f = function () {
  console.log('1');
}

function f() {
  console.log('2');
}

f() // 1

上面例子中,表面上后面声明的函数f,应该覆盖前面的var赋值语句,但是由于存在函数提升,实际上正好反过来。

7. arguments 和 this

每个函数都有 argumentsthis,除了箭头函数。

arguments:arguments 对象是一个包含所有普通参数的伪数组。 arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。

this: 如果不给任何条件,this 默认指向 window

function fn() {
  console.log(arguments)
  console.log(this)
}

如何传 arguments?

  • 调用 fn 即可传 arguments
  • fn(1,2,3); 那么 arguments 就是 [1,2,3] 伪数组

如何传 this?

  • 可以用 fn.call(xxx, 1,2,3)thisarguments
  • 而且 xxx 会被自动转化成对象。
function fn() {
  console.log(this)
}
fn.call(1)  //Number {1},如果传的this不是对象,JS会自动封装成对象

添加'use strict',就不会被转化成对象。

function fn(){
    'use strict'
    console.log(this)
}
fn.call(1)  //1

8. 关于 this

JS 在每个函数里加了 this。

let person = {
  name: 'frank',
  sayHi() {
    console.log(`你好,我叫` + this.name)  //this就是最终调用sayHi()的那个对象
  }
}

person.sayHi() 会隐式地把 person 作为 this 自动传给 sayHi,sayHi 可以通过 this 引用 person,以方便 sayHi 获取 person 对应的对象。

函数的两种调用方式

  • person.sayHi() 会自动把 person 传到函数里,作为 this
  • person.sayHi.call(person) 需要自己手动把 person 传到函数里,作为 this

当使用 call 调用函数而又没有用到 this 时,因为第一个参数要作为 this,但是代码里没有用 this,所以只能用 undefined 占位,其实用 null 也可以。

function add(x, y) {
  return x + y
}
add.call(undefined, 1, 2)  // 3

this 的两种使用方式

(1)隐式传递

  • fn(1,2) // 等价于 fn.call(undefined, 1, 2)
  • obj.child.fn(1) // 等价于 obj.child.fn.call(obj.child, 1)

(2)显示传递

  • fn.call(undefined, 1,2)
  • fn.apply(undefined, [1,2])

绑定 this

使用 .bind 可以让 this 不被改变。

function f1(p1, p2) {
  console.log(this, p1, p2)
}

let f2 = f1.bind({
    name: 'jack'
})
//f2 就是 f1 绑定了 this 之后的新函数

f2() // 等价于 f1.call({name:'jack'})

.bind 还可以绑定其他参数。

let f3 = f1.bind({name: 'frank'}, 'hi')
f3() // 等价于 f1.call({name:'frank'}, hi)

箭头函数

箭头函数没有 arguments 和 this。

箭头函数里面的 this 就是外面的 this (相当于一个普通变量),就算使用 call 也没有 this 和 arguments。

console.log(this)  // window

let fn = () => console.log(this)
fn()  // window
fn.call({name:'frank'}) // window

let fn2 = () => console.log(arguments)
fn2(1, 2, 3) // ReferenceError: arguments is not defined

立即执行函数(现在用的少)

ES 5 时代,为了得到局部变量,必须引入一个函数,但是这个函数如果有名字,就得不偿失。

于是这个函数必须是匿名函数,声明匿名函数,然后立即加个 () 执行它,但是 JS 标准认为这种语法不合法。

所以 JS 程序员寻求各种办法,最终发现,只要在匿名函数前面加个运算符即可,!~()+- 都可以,但是这里面有些运算符会往上走,所以推荐永远用 !来解决。

! function() {
  var a = 2
  console.log(a)
}()
//打印出2,并返回true

JS 新语法使用一个代码块和 let 即可声明一个局部变量。

{
  let a = 2
  console.log(a)
} //打印出2

console.log(a) //ReferenceError: a is not defined

学习链接

网道 JavaScript 教程