【JS】JS函数

506 阅读12分钟

函数是对象

一、函数的定义方式

1.用构造函数(正规写法,但是没人用)

let 变量名 = new Function('形式参数1','形式参数2',语句,'return 返回值')

  • 举例:let f = new Function('x,y','return x+y')
  • 所有的函数都是Function构造出来的
  • 包括Object、Arry、Function

2.具名函数

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

  • 举例:function add(x,y) {return x+y}
  • let f1 = function add(x,y) {return x+y}也可以把具名函数放到变量f1上,但此时用add这个名字调用这个具名函数却无法成功,add的作用域仅限于在等号右边(不懂,记住就行)

3.匿名函数

let 变量名 = function (形式参数1,形式参数2) {语句 return 返回值}

  • 因为匿名函数没有名字,所以必须给这个函数表达式赋值给变量。变量储存了这个匿名函数的地址而已
  • 把具名函数的函数名去掉就是匿名函数,也叫函数表达式(等号右边这部分)
  • 比如let f1 = function(x,y){return x+y}

4.箭头函数

let 变量名 = (形式参数) => {语句 return 返回值}

  • 因为箭头函数没有名字,所以必须把这个箭头函数赋值给变量f1。变量储存了这个箭头函数的地址而已
  • 形式参数在只有一个时,可以省略圆括号()
  • 只有返回值时,{}可以省。但是注意如果此时返回值是个对象,{}省了是会有bug的!那在对象外面加个()就可以了
let f1 = x => x*x

let f2 = (x,y) => x+y //圆括号不能省

let f3 = (x,y) => {return x+y} //花括号不能省

let f4 = (x,y) => {
    console.log('hi')
    return x+y
}

let f5 = x => ({name:x})

二、函数自身 fn VS 函数调用 fn()

1.函数自身 fn

fn是指函数本身

2、函数调用fn()

·fn()调用这个函数 只有给函数他需要的参数(也就是要给()圆括号)才可以调用执行这个函数

let fn = () => {console.log('hi')}
fn
//不会有任何结果,因为fn没有执行
let fn = () => console.log('hi')
fn()
//打印出hi,有圆括号才是调用
fn2  //  () => {console.log('hi')}
fn2()  //  hi

/*
fn保存了匿名函数的地址
这个地址被复制给了fn2
fn2()调用了匿名函数
fn和fn2都是匿名函数的引用而已
真正的函数既不是fn也不是fn2,而是() => {console.log('hi')}
*/

三、函数的要素

  • 调用时机:定义是定义了,但是调用是什么时候就根据什么时候的具体情况返回值
  • 作用域:就近原则
  • 闭包
  • 形式参数
  • 返回值
  • 调用栈
  • 函数提升
  • arguments (除了箭头函数)
  • this (除了箭头函数)

1.调用时机

let a = 1

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

fn()
a = 2
// 2
//过一会打印出a,过一会就是指要等所有代码执行完
let i = 0

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

//打印出6个6
//循环到每一个i都是过一会打印出i。过一会就是指要把所有循环走完。那么当循环走完,i就为6,所以要打印出6个6
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}

//打印出0、1、2、3、4、5
//为了迎合新人的思想特地做出了更新
//在JS中如果for和let一起用就会出现每次循环多创建一个i的情况

2.作用域

1.顶级作用域

2.局部作用域

  • 每个函数都会默认创建个局部作用域
  • 在局部作用域声明的变量是局部变量 3.全局变量
  • 在顶级作用域声明的变量是全局变量
  • window的属性是全局变量
  • 全局变量在任何作用域存在 4.其他都是局部变量
  • 局部变量只在局部作用域里存在生存 5.函数可以嵌套,作用域也可以嵌套

6.如果多个作用域有同名变量a,那么查找a的声明时,就向上看取最近的作用域(自己身处的那个作用域当然是最近的,没有再向上找最近的包的作用域),简称「就近原则」。查找a的声明的过程(也相当于作用域的范围)与函数执行无关,但a的值与函数执行有关 7.一个作用域与函数的执行无关叫静态作用域(词法作用域)

8.一个作用域与函数的执行有关叫动态作用域

3.闭包

如果一个函数用到了外部的变量,那么这个函数加这个变量,就叫做闭包

4.形式参数

  • 形式参数的意思就是非实际参数。
  • 形参可认为是变量声明。
  • 形参可多可少
function add(x, y){return x+y}
//其中X和y就是形参,因为并不是实际的参数
add(1,2)
//调用add时,1和2是实际参数,会被赋值给xy

5.返回值

1.每个函数都会有返回值。如果没写return,返回值就是undefined

2.函数执行完才会返回值

3.只有函数有返回值(有return啊)

4.其他的表达式都叫值,比如1+2的值为3

function hi(){ console.log('hi') }
hi()  //undefined
function hi(){ return console.log('hi') }
hi()  //undefined
      //因为返回值是console.log('hi')的值,而console.log()函数的返回值都是undefined,hi只是console.log()函数的打印值

6.调用栈

1、概念

(1)JS引擎在调用一个函数前,需要把函数所在的环境push到一个数组里(压栈),这个数组叫做调用栈

(2)JS 引擎退出一个函数之前,需要把环境从调用栈里弹出,然后回到这个弹出的环境,继续执行后续代码

2、举例

console.log(1)  //第1行
console.log('1+2的结果为' + add(1,2))   //第2行
console.log(2)   //第3行

3、递归函数(阶乘)

(1)阶乘

function f(n){
return n !== 1 ? n* f(n-1) : 1
}

(2)理解递归 (3)原理

  • 递归函数一般会出现自己调用自己的现象
  • 递归函数一般有一个递归出口,比如 n = 1 的时候,不再调用自己
  • 传递的过程其实就是压栈
  • 回归的过程其实就是弹栈
  • 递归几次就会压栈几次,比如f(4)要递归四次,就要压栈四次

4、一个调用栈最多可以压栈几次:调用栈的长度是有限的

Chrome 12578 Firefox 26773 Node 12536(Node和Chrome用的js引擎都是V8,所以差不多) 用下面代码可以测出调用栈的最多压栈次数

function computeMaxCallStackSize() {
try {
return 1 + computeMaxCallStackSize();
} catch (e) {
// 报错 说明stack overflow(爆栈)了 
return 1;
} }

5、超出最多压栈次数,就会爆栈,程序就会崩溃

7.函数提升

不管你把具名函数声明在哪里,他都会跑到第一行去

add(1,2)
function add(x,y){return x+y}
//3
//因为具名函数其实会跑到第一行,所以add函数以及被定义了。所以可以执行add(1,2)
let add=1
function add(x,y){return x+y}
//报错,因为具名函数其实在第一行,以及把add作为函数名了,之后let想要声明add是不行的,add已经被用了。(用var就很烦,所以不要用var)
add(1,2)
let  fn = function (x,y){return x+y}
// 报错,这个不算函数提升,因为这个函数被let声明并赋值,而let不允许先使用(add(1,2))后声明

8.arguments和this(除了箭头函数)

1、如何查看一个函数的thisarguments

(1)首要在这个函数里面写上打印出thisarguments

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

(2)查看arguments : 调用这个函数即可,即fn()

  • 可见,arguments是一个包含该函数所有普通参数的伪数组
  • 每次调用函数时,都会对应产生一个 arguments
  • 我们应该尽量不对 arguments 内的元素进行修改,修改 arguments 会让代码变得令人疑惑 (3)查看this
  • 调用这个函数,这时this表示window
  • 使用fn.call(x,1,2,3),那么第一个参数x会传给this(而且这个参数x会被自动转化为对象),后面的参数会传给arguments
  • 如果在(1)的代码中加上'use strict'就不会自动转化为对象了

2、arguments是普通参数,this是隐藏参数

3、理解的过程

代码一

let person = {  //声明了一个变量person,并且把一个对象赋值给这个变量person,那么实际上是变量person保存了这个对象的地址
  name: 'frank',   //这个对象有个属性name,属性值为frank
  sayHi(){        
  console.log(`你好,我叫` + person.name)  //这个对象还有个函数sayHi,函数的作用是打印出你好我叫+变量person的name属性的属性值
  } }
  
  //变量person保存了一个对象的地址,所以我们可以用这个变量person获取这个对象里的属性。这是我们一直知道的。这个叫引用
  //因为我们只是定义了这个函数sayHi,但是却没有调用它,所以在定义函数satHi时可以引用还没完全声明好的person变量。因为要是调用了这个函数,那么肯定变量person也早就声明好了。
  //一个函数在调用时里面的东西必须声明好,但定义时有没有声明好不重要
let person = {
name: 'frank',
sayHi(){
console.log(`你好,我叫` + this.name) } }

person.sayHi()
  • 我们想让函数中有一个变量,这个变量指向一个将要被创建的对象,这个对象中有个属性是我们需要。也就是我们想让函数可以引用一个对象
  • 当我们定义函数sayHi时,其实this被当做形式参数(隐形的):sayHi(this){}
  • person.sayHi()会被js引擎弄为person.sayHi(person)
  • 所以person就是实际参数,this就是形式参数
  • 所以this.name===person.name
  • 函数中有个变量this,这个变量指向将要创建的对象person,所以我们可用this指代person,并且使用它的属性

4、函数调用升级啦!

(1)小白调用法(再也不准用)

person.sayHi() 会自动把person作为this的实际参数传到函数里

(2)大师调用法

person.sayHi.call(xxx)

  • ()中的第一个参数xxx作为形式参数this的实际参数
  • 比如person

五、重新开始认识函数

(一)函数实际定义形式

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

(二)关于this

  • this是函数的隐藏参数
  • 在 new Fn() 调用中,this 指向新生成的对象,这是 new 决定的

(三)函数最正确的调用法(开发者通过call或apply显式指定的 this)

函数名.call(xxx,实际参数1,实际参数2)

函数名.apply(xxx,[实际参数1,实际参数2])

  • xxx是隐藏参数this的实际参数
  • 如果一个函数中没有用到this,就把xxx写为undefined或者nul或者'fuck',this默认指向 window,这是浏览器决定的

(四)以前的函数调用法(JS 的隐式传 this)的本质

1.函数名(实际参数1,实际参数2)

  • 等价于函数名.call(undefined,实际参数1,实际参数2)
  • 函数中没有用到this,所以把xxx写为undefined或者null或者'fuck',this 默认指向 window,这是浏览器决定的

2.yyy.函数名(实际参数1,实际参数2)

`

  • 等价于`yyy.函数名.call(yyy,实际参数1,实际参数2)
  • this默认指向yyy

(五)例子

我们用this写个遍历数组的forEach函数

Array.prototype.forEach2 = function(fn){  //forEach2函数是这样的:首先他需要接受个参数:函数fn
for(let i=0;i<this.length;i++){  //之后他会遍历数组
fn(this[i], i, this)   //每次都会调用函数fn,并且把this[i], i, this三个作为fn的实际参数
} }

因为我们把这个forEach写入了数组的原型中,所以所有数组都可以调用该函数。

arr = [1,2,3]
arr.forEach2.call(arr,(x,y,z)=>{console.log(x,y,z)})
//指定arr是this
arr.forEach2((x,y,z)=>{console.log(x,y,z)})
//浏览器自己认为arr是this
arr.forEach2.call({0:'bb',1:'cc',length:2},(x,y,z)=>{console.log(x,y,z)})
//指定一个伪数组是this

(六)用.bind绑定函数的参数

1.用.bind绑定函数的this

function f1(p1, p2){
console.log(this, p1, p2)
}
l
t f2 = f1.bind({name:'frank'})
// 那么f2就是f1绑定了this之后的新函数 
f2() // 等价于f1.call({name:'frank'})

2.用.bind绑定函数的其他参数

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

六、箭头函数

  • 箭头函数没有自己的this。箭头函数里面的this就是该函数外面的this,就算你加call都没用。 这多好啊,不用管这烦人的this了
  • 箭头函数没有arguments
console.log(this) //外面的this是window
let fn = () => console.log(this)
fn() // window
fn.call({name:'frank'}) // window

let fn1 = () => console.log(arguments)
fn3(1,2,3)
// Uncaught ReferenceError: arguments is not defined at fn1 

七、立即执行函数--生成局部变量的老方法

  • ES5时代,为了得到局部变量,必须引入一个函数
  • 但是这个函数如果有名字,就还会引入一个全局函数,得不偿失
  • 于是这个函数必须是匿名函数
  • 声明匿名函数,然后立即加个()执行它
  • 但是JS标准认为这种语法不合法
  • 所以JS程序员寻求各种办法
  • 终发现,只要在匿名函数前面加个运算符即可
  • !、~、()、+、-都可以
  • 但是这里面有些运算符会往上走
  • 所以推荐永远用!来解决
//推荐!ES5老方法
! function (){
var a =2
console.log(a)
} ()
//更推荐!ES6最新方法
{
    let a = 2
}
//不推荐()
console.log('hi')  //如果立即执行函数前面这句代码
(function (){
var a =2
console.log(a)
} ()) //报错:console.log(...) is not a function


//因为js中回车没意义,所以把下面的立即执行函数接到前面去了,所以相当于
undefined(function (){
var a =2
console.log(a)
} ()) //把undefined当一个函数来执行了,哪来的undefined这个函数,所以报错


//补救措施
console.log('hi');  //在他俩之间加分号。注意这是js语言中唯一两句代码之间需要加分号的情况!!!
(function (){
var a =2
console.log(a)
} ())