JS函数对象

1,374 阅读5分钟

函数提升

当函数定义时,不管在哪一行,都会优先被js解析,就相当于在第一行。这就是函数提升。

但是注意

f() // TypeError: undefined is not a function
var f =function(){}

这种是函数表达式赋值,不是声明一个函数,这种形式不会产生函数提升。

相当于

var f
f()  // TypeError: undefined is not a function
f=function(){}

现在我来考考你,下面的代码会怎样出现什么?

var f =123;
function f(){
console.log(123)
}
f //f是谁?

虽然表面来看,应该是下面的函数f覆盖上面的var产生的f,但是由于函数提升的原因,结果刚好相反。f是123。

整个过程如下:

var f //这里的var提升被函数覆盖
function f(){}
f=123
f //f就是123

函数属性

name属性

函数的name属性返回函数名,这是js自带的

function f(){}
f.name //"f"
var a =function a(){}
a.name //"a" 通过赋值形式的函数声明的name就是变量名

name的另一个作用,就是获取参数的名字

var fun =function(){}
function fn(f){
	console.log(f.name)
}
fn(fun) // fun

上面代码获取到参数f的name,打印出参数是什么函数

length属性

函数的length属性可以打印出函数定义时形参的个数

function fn(a,b,c){}
console.log(fn.length) //3

不管调用时有多少实参,length始终是3

toString

函数的toString方法可以返回函数的源码,如果是原生函数,则返回原生代码

function fn(){console.log(1)}
fn.toString() //"function fn(){console.log(1)}"
Math.min.toString() // "function min() { [native code] }"

函数的调用时机

求以下函数打印出的a

let a = 1
function fn(){
  console.log(a)
}

函数没调用,无法打印

let a = 1
function fn(){
  console.log(a)
}
fn()

打印出1

let a = 1
function fn(){
  console.log(a)
}
a = 2
fn()

打印出2

let a = 1
function fn(){
console.log(a)
}
fn()
a = 2

打印出1

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

打印出2,因为回调函数在队列栈中

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

打印出6个6

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

打印出0,1,2,3,4,5。因为let在块级作用域中有隐藏作用域,当循环时,每个作用域中let i = 作用域的i ,这样打印的就是0、1、2、3、4、5。

以上例子都是为了证明函数的调用时机非常重要。

函数作用域

每个函数都有作用域,在顶级作用域下定义变量,是全局作用域。在函数内定义变量,则是局部作用域。函数执行时所产生的作用域,是定义函数时产生的,而不是调用函数所产生的。

var x = function () {
  console.log(a);
};

function y(x) {
  var a = 2;
  x();
}

y(x)
// ReferenceError: a is not defined

上面代码中的a不会被x()所用,因为x函数的作用域绑定在全局作用域,不能获取到y函数内的变量a。

挂在window下的变量都是全局变量。函数都是可以嵌套的,如果在不同作用域下定义相同变量名,则会优先查找最近一级作用域下的变量。

function f1(){
let a = 1
function f2(){
  let a = 2
  console.log(a)//2
}
  console.log(a)//1
  a = 3
  f2()
}
f1()

闭包

虽然闭包这个名词很屌,但是实际上无非就是内层函数访问到外部变量而已。如果一个函数访问到外部变量,那么这个函数加这个变量,就是闭包。 闭包

形参

对于什么是形参,我们都已经知道了。在js中,形参可多可少,形参只是给参数取名字而已,实际上形参只是arguments的语法糖。

在js中,即使不传参数,也并不会报错。

let Fn = function (x, y) {
            console.log(x + y)
        }
        Fn() //NaN

如果不想要第一个参数,只想要第二个参数,那么只能显式传入undefined

function fn(x,y){
	console.log(y)
    }
    fn(undefined,2) //2

传递方式

如果传递的参数是简单数据类型(string、number等),那么传递的是变量名保存的值,在函数内修改它不会影响函数外部的原始值。

function fn(x){
	x=3
}
var x=2
fn(x)
x // 2

上面代码里:外部变量的x是简单数据类型,保存了2这个值。在函数内部的x实际上是外部x的浅拷贝,无论怎么修改,都不会影响到外部x的值。

如果传递的参数为复杂数据类型,那么在函数内部进行修改,就会对外部的原始值产生影响。

var x={name:'yyy'}
function fn(x){
	x.name='xxx'
}
fn(x)
x //{name: "xxx"}

这是因为复杂数据类型传递的值为数据的保存地址。因此,在函数内部对数据进行修改也会影响到原始值

但是如果在函数内部对传递的复杂类型参数进行整个替换,那么就不会影响到传递参数的原始值。

var x={name:'yyy'}
function fn(x){
	x=1
}
fn(x)
x //{name: "yyy"}

这是因为在函数内部的x实际上只是外部x的浅拷贝,在函数内部只是重新让其指向其他地址,而外部的x始终保存了原始值的地址,所以不会对原始值有任何改变。

返回值

只有函数才有返回值。console.log的返回值是undefined。返回值需要用return关键字进行返回

调用栈

当调用一个函数时,会自动将函数的环境压进调用栈中,当函数执行完之后,才会弹出这个环境,继续执行下一行代码。 例如:

console.log(function(){return 123}())

在执行log函数时,会将log函数先压入调用栈里,再进入立即执行函数,这时候再把立即执行函数的环境压入调用栈里。等执行完立即执行函数后,弹出立即执行函数的环境再弹log函数的环境。

调用栈 使用递归函数会出现爆栈的情况,也就是说压进调用栈的函数环境不能超过浏览器的承受值。一般来说chrome最多承受约12000左右的压栈。

递归函数

简单来说递归函数就是自己在内部调用自己,递归函数需要有一个终止点。

//写一个阶乘递归函数
function Fn(n){
n!===1? n * Fn(n-1):1
}

递归阶乘