【JS全解】JS函数

183 阅读9分钟

函数对象

  • 函数是一种对象。

新建函数

具名函数

  • 语法:
    function 函数名(形参1, 形参2){
    	语句
    	return 返回值
    }
    
  • 具名函数的作用域
    let a = function fn(a, b){return a + b}
    fn(1, 2) // fn is not defined
    function fn(a, b){return a + b}
    fn(1, 2) // 3
    
    • 如果函数声明在等于号右边,那么fn()的作用域只有等于号右边,这种情况下想要调用函数只能使用a()。
    • 这种情况下声明出来的函数仍是具名函数,它的name值为'fn',但是只能通过a()调用。

匿名函数

  • 具名函数去掉函数名就是匿名函数:
    function(形参1, 形参2){
    	语句
    	return 返回值
    }
    
  • 也叫函数表达式。
  • 匿名函数和真正的函数是不同的,前者只是函数地址的引用:
    function trueFn(){return 'hello world'}
    let a = () => 'hello world'
    console.dir(trueFn) // ƒ trueFn()
    console.dir(a) // a()
    

箭头函数

let f1 = x => x * x
let f2 = (x, y) => x + y // 圆括号不能省略
let f3 = (x, y) => {return x + y} // 花括号不能省略
let f4 = (x, y) => ({name: x, age: y}) //直接返回以花括号包裹的对象会报错
  • 箭头函数也是匿名函数。
  • 如果有2个及以上参数,则圆括号不能省略。
  • 如果函数体有2个以上语句,则花括号不能省略。这种情况下return不能省略。
  • JS中优先把花括号视为函数,因此直接返回花括号包裹的对象会报错。这种情况下要使用圆括号把圆括号包裹起来。

Function()

let fn = new Function('x', 'y', 'return x+y')
  • 基本没人用。

函数自身和函数调用

  • 函数自身
    let fn() = () => console.log('hello world')
    fn
    
    • fn没有任何效果,因为fn函数没有被调用。
  • 函数调用
    let fn() = () => console.log('hello world')
    fn() // hello world
    
    • 有圆括号才是函数调用。

函数的引用

let fn = () => console.log('hello world')
let fn2 = fn
fn2() // hello world
  • fn并不是真正的函数,它只是保存了匿名函数的地址,并将地址复制给了fn2。
  • fn和fn2都只是匿名函数的引用,不是真正的函数。

函数的要素

调用时机

  • setTimeout()
    let a = 1
    function fn() {
    	setTimeout(() => {
    		console.log(a)
    	}, 0)
    }
    fn() // 2
    a = 2
    
    • setTimeout()的计时会在所有可执行代码执行完毕后才开始,无论给的延迟参数有多小,哪怕是0,
  • let-for-setTimeout也是遵循上述规则。
    let i
    for(i = 0; i < 6; i++){
    	setTimeout(()=>{
    		console.log(i)
    	}, 0)
    }
    // 6 6 6 6 6 6
    
  • 除了for-let-setTimeout,这是刻意设计的例外。
    for(let i = 0; i < 6; i++){
    	setTimeout(()=>{
    		console.log(i)
    	}, 0)
    }
    // 0 1 2 3 4 5
    
    • JS在for-let语句中,每次循环会多创建一个i。
    • console.log()在正式开始执行的时候,每次都会调用不同的i。

作用域

作用域概述

  • 通常来说,一段程序代码中所用到的名字并不总是有效和可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。 作用域的使用提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。
  • 全局作用域:全局有效。作用于所有代码执行的环境(整个script标签内部)或者一个独立的JS文件。
  • 局部作用域:局部有效。仅作用于函数或代码块的内部。

变量的作用域

  • 全局变量:在全局作用域中声明的变量。全局变量在任何区域都可以访问和修改。
    • window的属性就是全局变量,其他的都是局部变量。
    • 如果是通过window.变量名来赋值的,在任何区域都可以进行变量声明。
  • 局部变量:在函数或代码块内部声明的变量。局部变量只能在当前花括号内部访问和修改。

作用域链

  • 只要是代码,就至少有一个作用域。
  • 如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。
  • 根据在局部作用域内部可以访问外部变量的这种机制,就称作作用域链,采取就近原则的方式来查找变量最终值。
  • 就近原则:如果多个作用域有同名变量a,在查找a的声明时,就向上取最近的作用域。

作用域的特殊情况

  • 在局部或块级作用域内部,没有声明的情况下直接给变量赋值,也当做全局变量看。
  • 但是函数内部的形参可以看做是函数内部的局部变量,而不会被视为全局变量。

闭包

  • 如果一个函数用到了外部的变量,那么这个函数加这个变量,就叫做闭包
function f1(){
	let a = 1
	function f2(){
		console.log(a)
	}
}
f1()
  • 这里的a和f2()就组成了闭包

形式参数

  • 形式参数:声明函数时写在fn()里的叫形参。
  • 实际参数:调用函数时写在fn()里的叫实参。
  • 形参可以视为函数内部的变量声明(let a),实参可以视为给这个变量赋值(a = 1)。
  • 实参的传递实际上是把stack中的内容复制了一份,赋值给形参。

返回值

  • 每个函数都有返回值,只有函数才有返回值。
  • 没写return的情况下,返回值为undefined。
  • 返回值≠输出值,console.log('hi')的输出值为'hi',返回值为undefined。
  • 函数执行完了才会有返回值。

返回值和值

  • 返回值和值在调用出后的效果是一样的。
  • 返回值和值的区别在于:需不需要通过函数来调用。
  • 在JS语境下,由于没有私有成员。对象内部的属性,能够被直接调用,一般来说都是值。而非对象内部的属性,大部分情况下需要函数来调用,一般来说都是返回值。

调用栈

  • JS引擎在调用一个函数前,需要把函数所在的环境push到一个数组里(压栈),这个数组叫做调用栈
  • 等函数执行完了,就会把环境pop出来,然后return到之前的环境(弹栈),继续执行后续代码。
  • 调用栈的作用在于记录每次压栈前的节点,方便弹栈时能回到原来的节点。
  • 这里的push、pop、return不是JS中的,而是更底层的。
  • 调用栈是否会被压满?
    • 如果使用递归函数,很有可能会把调用栈压满。
  • 测试调用栈上线的代码:
    function computeMaxCallStackSize() {
      try {
        return 1 + computeMaxCallStackSize();
      } catch (e) {
        // 报错说明 stack overflow 了
        return 1;
      }
    }
    
  • 爆栈:如果调用栈中压入的帧过多,程序就会崩溃。Chrome的压栈上限约为11k~12k次。

函数提升

  • 只要通过function fn(){}来声明具名函数,不管把函数声明在哪里,它都会跑到第一行。
    add(1, 2) // 3
    function add(x, y) {
    	return x+y
    }
    
  • 如果同时有function fn(){}var fn = 1,也是先执行函数声明,再指定变量声明。
    var fn = 1
    function fn(){}
    console.log(fn) // 1
    
  • 如果同时有function fn(){}var fn,此时由于var只是声明,并没有给定具体的内容,而function()在声明的同时会给与内容,因此function()赋值成功且不会被var覆盖。
    var fn
    function fn(){}
    console.log(fn) // ƒ fn(){}
    
  • 变量声明并且赋值函数的情况下,语句会被视为变量声明语句,不会发生函数提升:
    fn(1,2)
    let a = function(a, b){return a + b} // fn is not defined
    

arguments和this

  • 每个函数都有arguments和this,除了箭头函数。

argument

  • 包含函数所有参数的伪数组。
  • 如何给argument赋值:调用函数即可。
  • fn(1,2,3)中,argument就是[1,2,3]伪数组。
  • 尽量不要对arguments内的元素进行修改。

this

  • this的作用:在函数怎么得到未来要创建的对象的引用,this关键字用来表示对象本身。
  • JS中this存在的问题:JS中直接传递对象本身,如person.sayHi(person)这种写法会出现语法错误。
  • 如果不给任何条件,this默认指向window。
  • 如何给this赋值:fn.call(thisArg, arg1, arg2, ...)传递this和argument。
  • 如果传递给this的thisArg不是个对象,JS会自动转换成对象。通过在函数体开头添加字符串‘use strict’可以避免:
    function fn(){
    	'use strict'
    	console.log(this)
    }
    fn.call(1) // 1
    
  • this的调用形式
    • 隐式传递:自动的把对象本身传到函数中作为this,例:person.sayHi()
    • 显式传递:推荐,使用call()手动把对象传到函数中作为this() ,例:person.sayHi.call(person)
  • 注意
    • new fn()中,this指向的是新生成的对象,也因此如this.name = name这类语法可以成立。

call()

  • 用call()来传递this:
    let obj = {
    	str: 'hello world',
    	sayHi(){
    		console.log(this.str)
    	}
    }
    obj.sayHi.call(obj)
    
  • 在使用call()时,即不使用this,也要传递一个undefined或null来占位。
    function add(x,y){
      return x+y
    }
    add.call(undefined, 1,2) // 3
    
  • Array.prototype.forEach()也是用this来写的:
    Array.prototype.forEach = furnction(fn){
    	for(let i = 0; i < this.length; i++){
    		fn(this[i], i)
    	}
    }
    

bind()

  • 使用bind()可以绑定this,让this不被改变:
    function f1(p1, p2){
    	console.log(this, p1, p2)
    }
    let f2 = f1.bind({name: 'frank'})
    
    • f2()就是f1()绑定了this之后的新函数。
    • f2()等价于f1.call({name: 'frank'}),只不过this已经被绑定了。
  • bind()可以绑定所有的参数:
    let f3 = f1.bind({name: 'frank'}, 'hello')
    f3('world') // {name: 'frank'} 'hi' 1
    
    • f3()等价于f1.call({name: 'frank'}, 'hello')

箭头函数

  • 箭头函数没有argument和this。
  • 箭头函数中的this就是window,即使call()也无法改变箭头函数的this。

立即执行函数

  • 过去,想要在不声明全局变量的情况下,得到一个局部变量。通过声明匿名函数并直接调用,可以不生成全局变量:
    ! function(){
    	let a = 'hello world'
    	console.log(a)
    }()
    
  • 但是function(){}()会被JS引擎识别错误,通过在function()前增加特殊标识符可以避免这个问题:
    + function(){}()
    - function(){}()
    * function(){}()
    ! function(){}()
    
    • 最好使用!,其他符号都有可能产生BUG,比如+会尝试把函数同字符串拼接起来。
  • 但是在新版JS中,直接通过代码块就可以解决:
    {
    	let a = 'hello world'
    	console.log(a)
    }
    

拓展

静态作用域

  • 函数执行对变量查找没有关系的作用域。
  • 这种作用域中,增加或删除函数不会影响原有的变量查找。
  • JS的作用域都是静态作用域。

递归函数

function f(n) {
	return n !== 1 ? n*f(n -1) : 1
}
  • 递归:先递进,再回归。
  • 递进的过程实际就是压栈的过程。
  • 回归的过程时机就是弹栈的过程。