【JavaScript】函数

242 阅读6分钟

一、定义

1、什么是函数?

函数是一种对象。

2、如何定义一个函数?

具名函数

function 函数名(形参1,形参2){  
	语句  
    return 返回值  
}

匿名函数

let a = function(x,y)(return x+y)

  • 将具名函数去掉函数名即可,也叫函数表达式
  • 等号右边的部分叫函数表达式
  • 左边的为变量,保存函数的地址

let a = function fn(x,y){return x+y}

  • 函数名fn的作用域局限于等号右边,并不是全局

箭头函数

let f1 = x => x*x

let f2 = (x,y) => x*y

let f3 = (x,y) => {
	console.log('hello')
	return x*y
}

let f4 = x => {name:x} //报错,{}会被当作代码块的符号使用
let f4 = x => ({name:x}) //正确,圆括号的加入使得{name:x}成为一个对象返回给x

构造函数

let f = new Function('x','y','return x+y')

  • 基本没人用,但是可以让你我了解到所有函数都是Funtion构造出来的
  • 包括Object、Array、Function

3、函数自身&函数调用

let fn = () => console.log('hello')

  • 输入fn,没有输出,因为fn是函数本身
  • 输入fn(),输出'hello',因为fn()是函数执行/调用

二、函数的要素

函数的要素 = 每个函数都有的东西

1、调用时机

let i = 0
for(i=0;i<6;i++){
	setTimeout(() => { //setTimeout是等一会
    	console.log(i)
    },0)
}

打印出6个6,而不是1,2,3,4,5,6;因为setTimeout函数会让console.log()在for循环全部执行完后执行

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

打印出0,1,2,3,4,5,而不是6个6,因为JS会在for、let一起用的时候加东西,每次循环会多创建一个i

2、作用域

全局作用域

  • 一个函数中用let、const声明的变量是局部变量,仅在包裹变量的{}中有效
  • 顶级作用域中声明的变量是全局变量,比如let a = 1,
  • 挂到window上的属性是全局变量,比如window.c = 10,且window.c不需要写在最外边,可以写在任何地方,都有c=2
  • 为什么Object可以随意使用,就是因为它是挂在window上的属性,作用域是全局

局部作用域

  • 局部作用域可以嵌套
  • 如果多个作用域有同名变量a,查找a的声明时,向上取最近的作用域,简称就近原则
  • 作用域与函数的执行与否无关,即静态作用域

3、闭包

定义:如果一个函数用到了外部的变量,那么这个函数和这个变量叫做闭包,下面代码中的a和f3组成了闭包

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()

4、形式参数

定义:不是实际参数。
形参的本质就是变量声明

function add(x,y){
	return x+y
}
  • x,y是形参
  • 调用add时,1和2是实际参数,会被复制给x,y
  • 上面的代码近似等价以下代码
function add(){
	var x = arguments[0]
    var y = arguments[1]
    return x+y
}

5、返回值

  • 每一个函数都有返回值
  • 只有函数有返回值,1+2的值为3(1+2不是函数)
  • 函数执行完毕后才有返回值
function hi(){console.log('hi')}
hi() //没写return,所以返回值为undefined

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

6、调用栈

定义

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

递归函数(举例阶乘)

function fn(n){
	return n === 1 ? 1 : n*f(n-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

先递进,后回归。
递进是压栈的过程,回归是弹栈的过程。
f(4)压栈4次

爆栈

如果调用栈中压入的帧太多,程序就会崩溃

7、函数提升

定义

  • 不管你把具名函数声明在哪里,它都会被提升到第一行优先执行
  • 使用let声明的匿名函数不存在函数提升,let是赋值语句

8、arguments和this

除了箭头函数,每个函数都有arguments和this。二者都是JS原创的部分,但是并不优秀。

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

如何传arguments?

  • 调用fn即可传arguments,它是包含所有参数的伪数组
  • fn(1,2,3),则arguments就是伪数组[1,2,3]

如何传this?

  • 只能用fn.call()传入this
  • 当传入undefined或空时,this会指向window
  • 可用fn.call(xxx,1,2,3)同时传this和arguments
  • 如果传入的this不是对象,会被自动转化为对象(JS的糟粕)
  • 如果xxx是空或者undefined,xxx会变为window
  • fn.call(1,2,3,4),1是this,2,3,4则是arguments

如何使传入的this不被自动封装成对象?

function fn(){
	'use strict' //添加这行代码
    cosole.log(this)
fn.call(1) //返回数字1,而不是对象{1}

(8)、如何理解 this?

假如没有this

let person = {
	name:'lu',
    sayHi(){
    	console.log('你好,我叫'+person.name)
    }
}

person.name //返回'lu'

  • 用直接保存了对象地址的变量person来获取name
  • 这种方法叫做引用

以上方法的问题一

let sayHi = function(){
	console.log('你好,我叫' + person.name)
}
let person = {
	name:'frank',
    'sayHi':sayHi
}
  • 先定义函数sayHi,再定义对象person
  • person如果改名,函数就挂了
  • 我们不希望sayHi函数中出现person的引用

以上方法的问题二

class Person{
	constructor(name){
    	this.name = name //这里的this是new强制指定的
    }
    sayHi(){
    	console.log(???)
    }
}
  • 这里只有类,没有创建对象,不可能获取对象的引用
  • 那么如何拿到对象的name属性?

所以,如何在不知道对象名的情况下拿到这个对象的引用?

一种比较笨拙的方法

//对象
let person = {
	name:'lu',
    sayHi(p){
    	console.log('你好,我叫' + p.name)
    }
}
//类
class Person{
	consturctor(name){
    	this.name = name
    }
    sayHi(p){
    	console.log('你好,我叫' + p.name)
    }
}

Python的解决方法

class Person:
	def __init__(self,name):
    	self.name = name
    def sayHi(self):
    	print('Hi,I am' + self.name)
person = Person('frank') 
person.sayHi()
  • 每个函数都接受一个额外的self,这个self就是待创建的对象
  • person.sayHi() 等价于 person.sayHi(person),person 被传给 self了
  • 得到一个未来的对象的引用

JavaScript的解决方法:发明一个关键字this,来获取那个未来的对象

let person = {
	name:'frank',
    sayHi(){ //关键字this,代表以后你创建的那个对象
    	console.log('你好,我叫' + this.name)
    }
}
  • person.sayHi() 相当于 person.sayHi(person),隐式地把 person 作为 this 传给 sayHi()
  • person 是个地址,它被传给 this 了
  • 这样,每个函数都能用 this 获取一个未知对象的引用了

如何调用?

  • 小白调用法:person.sayHi(),自动将person传入函数,作为this
  • 大师调用法:person.sayHi.call(person),手动将person传入函数,作为this

默认使用大师调用法,这样可以清晰地看到传入的this是什么,举例:

let person{
	name:'lu',
    sayHi(){
    	console.log(this.name)
    }
}

person.sayHi.call({name:1}) //这里指定传入的this为{name:1},所以打印出的this.name为1
>1

9、this的两种使用方法

隐式传递

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

显式传递

  • fn.call(undefined,1,2) //此时this是undefined,形参是1和2
  • fn.apply(undefined,[1,2])

10、绑定this

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

function f1(p1,p2){
	console.log(p1,p2)
}
let f2 = f1.bind({name:'lu'}) //f2就是一个将this固定为{name:'lu'}的新f1,等价于f1.call({name:'lu'})

.bind还可以绑定其他函数

let f3 = f1.bind({name:'lu'},'hello') //等价于f1.call({name:'lu'},'hello'),这里的'hello'就是第一个形参p1

11、箭头函数

没有arguments和this,箭头函数自身改变不了this,只拿外面的this

let fn = () => console.(this)
fn()
> Window