JS中的函数的所有知识点

592 阅读12分钟

函数的五种声明方式


具名函数

function name(x,y){
} 
  • 函数会自动加上return undefined
  • consolo.log()永远返回undefined

匿名函数(函数表达式)

var a
a=function (x,y){
} 
  • 不能单独使用,必须赋值给一个变量

具名函数赋值

var a
a=function name(x,y){
} 

tips:其中 name 为函数标示符,只在函数体内可访问,辣鸡

构造函数声明

var a=new Function('x','y','函数体')

箭头函数

  1. 标准语法

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

  2. 如果只有一个函数体,return和花括号可以省略,同时省略

    let a=(x,y)=>x+y

  3. 如果只有一个参数,圆括号可以省略,同时省略return

    let a=n=>n*n

为什么要出现箭头函数

因为this大家看不懂-。-,箭头函数没有this,如果你使用this,那么箭头函数里面的this就会像变量一样,从外面去找

函数的name


function f(){}
//f.name='f'

var f1=function(){}
//f1.name='f1'

var f2=function f3(){}
//f2.name='f3'

var f4=new Function('x','y','return x+y')
//f4.name='anonymous'

js是真滴辣鸡,兄弟,当做函数内部的名字优先级高吧

函数的调用


var a=function(){}
a()===a.call(this,argumens)

左边是是右边的语法糖,右边的才是真正的调用,call牛逼

call stack 调用堆栈


call stack详见

由于js是单线程的,所以js在执行一长串代码的时候,它会把当前的环境都记住,当它看一个函数的时候,它将进入函数内部,并且留下记号,记录位置,放在调用栈顶部,执行完成后再从栈中移除这个函数
函数的调用机制,先进后出,也就是LIFO(last in first out) 栈的数目是有限的,当你堆栈上限溢出时,就会stack overflow

function c() {
console.log('c');
return 'c'
}

function b() {
console.log('b');
c();
return 'b'
}

function a() {
console.log('a');
b();
return 'a'
}

a.call();

执行过程a.call→console.log('a')→b.call()→console.log('b')→c.call()→console.log('c')→return 'c'→returbn 'b'→ return 'a'

callback


回调详见

  • 名词形式:被当做参数的函数就是回调,必须要另外一个函数B里面调用这个函数参数A
  • 动词形式:调用这个回调
  • 注意回调跟异步没有任何关系
  • 举个栗子,我要买个手办,给淘宝老板说,有货了喊我,那么我给老板留个电话,喊他有货了call我,那么在代码中就是这样
    我传一个函数A(我的电话号码)给函数B(老板的店铺),老板一有货了,就会在函数B(老板的店铺)中call函数A(给我打电话),这就是回调。

Don't call me; I'll call you!

词法作用域


又名静态作用域
传说中的在函数中声明变量不加var是声明全局变量[(a=1),优先当作赋值好吧],错误!
如果作用域中没有当前变量的声明,把a=1当作赋值,然后找他父作用域,如果沿着作用域树找,如果全局范围内有a,就是a=1的赋值,如果全局范围内都没有当前变量的声明,就会当成全局变量的声明并赋值!!

var a=1
function f(){
    console.log(a)//a=2
}

a=2
f.call()

!!!!作用域是指,我们能确定的是f中的a是那个的a,但是并不能确定他的值!!!

分析代码是先提升变量

变量提升


看到一个挺好的例子,引用一下 函数声明和变量声明都会提升,且函数声明更置顶

var a = 100
function a(){
    console.log(a)
}
a()//a is not a function

上面的代码等于

function a(){
    console.log(a)
}
var a
a = 100
a()

a被重新赋值了,所以a is not a function

foo()//TypeError
bar()//ReferenceError
var foo = function bar(){
    //
}

可以看成这种形式

var foo
foo()
bar()
forr = function (){
    //
}

根本原因

  1. 函数声明会提升
  2. 变量声明也会提升
  3. 函数表达式不会提升
  4. 函数声明比变量声明更置顶:(函数在变量上面)
  5. 变量和赋值语句一起书写,在js引擎解析时,会将其拆成声明和赋值2部分,声明置顶,赋值保留在原来位置
  6. 声明过的变量不会重复声明

执行上下文和作用域

概念

  • 作用域:作用域代表着已声明变量或者函数的访问范围,在作用域内使用变量会先从当前作用域寻找,如果没有会往作用域上端寻找,直到全局作用域
  • 执行上下文:每一个函数都有自己的执行上下文EC(执行环境 execution context),并且每个执行上下文中都有它自己的变量对象VO(Variable object),用于存储执行上下文中的变量 、函数声明 、函数参数,这解释了js如何找到我们定义的函数和变量。并且函数是js中唯一一个能创建出作用域的(ES5)
  • 作用域和执行上下文之间最大的区别是:执行上下文在运行时确定,随时可能改变;作用域在定义时确定,永远不会改变。
  • V0和A0:未进入执行阶段之前,变量对象(VO)中的属性都不能访问!但是进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。 它们其实都是同一个对象,只是处于执行上下文的不同生命周期

执行上下文

又称词法环境,它是存储和处理数据的栈,主要分为两部分
一个例子来说明:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);
  • 预处理阶段:
  1. 在这个阶段它会将所有的var类型的变量存入上下文中并给它赋值为undefined;
  2. 此之外,它还会将声明的函数存入栈中,并将函数体赋值给它。
  3. 还有一个就是this对象
  4. 函数上下文的变量对象初始化只包括 Arguments 对象
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}
  • 执行阶段:
  1. 给词法环境中的变量进行赋值
  2. 如果词法环境中不存在这个变量,就把它加入到词法环境并给它赋予相应的值。
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

函数执行上下文中作用域链和变量对象的创建过程

以下面这个函数为例

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
  1. 函数被创建保存作用域链到内部属性[[scope]
checkscope.[[scope]] = [
    globalContext.VO
];
  1. 执行函数,创建函数执行上下文,函数的执行上下文被压入执行上下文栈
ECStack = [
    checkscopeContext,
    globalContext
];
  1. 函数并不立即执行,开始做准备工作,复制词法环境所生成的作用域链到自己的作用域链
checkscopeContext = {
    Scope: checkscope.[[scope]],
}
  1. 用arguments 创建AO,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}
  1. 将AO压入作用域链顶端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
  1. 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}
  1. 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
    globalContext
];

闭包


如果一个函数使用了它范围外的变量,那么这个(函数+这个变量)=闭包
意义:隐藏变量,操作一个你访问不到的变量

function foo(){
  var local = 1
  function bar(){
    local++
    return local
  }
  return bar
}
foo()()

ECMAScript中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:
    即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    在代码中引用了自由变量
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

执行过程

进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
全局执行上下文初始化
执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
checkscope 执行上下文初始化,创建变量对象、作用域链、this等
checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
f 执行上下文初始化,创建变量对象、作用域链、this等
f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

什么是this


this就是call的第 一个参数
this必须是对象,this是对象和函数之间的羁绊,一切都是命运石之门的选择呀,岂可修.

var a=function(){}
a.call(this,arguments)

tips:在非严格模式下,null和undefined的this值会自动指向window

  • this就指向调用函数的那个对象

  • Array.prototype.slice.call(arguments)可以将伪数组转化为数组,因为arguments是个伪数组没有slice方法,我们call(arguments)把this指向这个对象,相当于让arguments有了slice这个方法(arguments.slice())

  • 那么Array.prototype.slice.call(arguments,1);这句话的意思就是说把调用方法的参数截取出来。(arguments.slice(1))

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1
console.log(foo.bar())//2
//示例2
console.log((foo.bar)())//2
//示例3
console.log((foo.bar = foo.bar)())//1
//示例4
console.log((false || foo.bar)())//1
//示例5
console.log((foo.bar, foo.bar)())//1

|| , =等运算符会直接 返回一个匿名函数 function () { return this.value;} ,那么就相当于变成了全局调用,那么this在非严格模式下指向window

判断this,关键在于判断表达式结果是否是Reference类型,深入了解

Reference

这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中

Reference由三个部分组成
base value
referenced name
strict reference

举个例子

var foo = 1;

// 对应的Reference是:
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

再来一个

var foo = {
    bar: function () {
        return this;
    }
};
 
foo.bar(); // foo

// bar对应的Reference是:
var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};

获取 Reference 组成部分的方法,比如 GetBaseIsPropertyReference

  1. GetBase

返回 reference 的 base value。

  1. IsPropertyReference

简单的理解:如果 base value 是一个对象,就返回true。

判断是否是Reference

1. 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
2. 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
3. 如果 ref 不是 Reference,那么 this 的值为 undefined

call/apply/bind


  • 用来改变this的指向
  • call()apply()的区别在于就是call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组或者为伪数组(要有lenght).
  • bind()不会立即执行,bind()相当于创建一个新的函数,与调用函数拥有相同的函数体,同时,当新函数被调用时 this 值绑定到 bind() 的第一个参数,同时调用时的参数被提供给新函数.

高阶函数


  • 在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
  • 接受一个或多个函数作为输入:forEach sort map filter reduce
  • 输出一个函数:lodash.curry
  • 不过它也可以同时满足两个条件:Function.prototype.bind
  • 高阶函数的作用:可以任意组合函数

arguments


auguments是函数的参数,是个伪数组,not array。 我们可以使用一些技巧把他变成真数组

 let args=[].slice.call(arguments)    //ES5
 let args=Array.from(arguments)      //ES6  
 let args=[...arguments]             //ES6

new的作用


new的本质是Brendan Eich为了js模拟类的一个语法糖

  1. 创建一个空对象,
  2. this=空对象
  3. 绑定原型,this.proto===构造函数的.protype
  4. 构造函数.protype={constructor:构造函数A}
  5. 执行fn.call(this.arguments)
  6. return this
function 构造函数A(x){
    this.属性a=x
    this.属性b=xxxxxxxxx
    var temp={}
    this=temp
    构造函数A.protype={
        constructor:构造函数A,
        属性a:()=>{},
        属性b:()=>{},
        ...
    }
    this.__proto__ === 构造函数A.protype
    return this
}

prototype就是共有属性

constructor 属性

我们需要记录下来是临时对象是由哪个函数创建的,默认在构造函数.protyped的属性中加了一个constructor 如果我们直接对protype赋值,那么这个constructor这个属性将覆盖掉,所以我们直接在protype上加属性,或者赋值的时候添加constructor属性

构造函数A.protype={
    constructor:构造函数A
    a:a,
    b:b,
    c:c,
}
------------我4分割线--------------
构造函数A.protype.a=a
构造函数A.protype.b=b
构造函数A.protype.c=c

立即执行函数

在es6 的let出现之前,使用局部变量得这样

立即调用函数

避免全局污染

  1. (function(){}).call()
  2. (function(){}.call())
  3. -function(){}.call()
  4. +function(){}.call()
  5. !function(){}.call()
  6. ~function(){}.call()

立即执行函数用来创建局部作用域,闭包用来间接访问一个变量,但是不能直接操作这个变量,

这个可以return在赋值给全局变量,也可以直接赋值给window

!function (){
  var local = 1
  window.bar=function (){
    local++
    return local
  }
}.call()

bar()
var bar=(function (){
  var local = 1
  function bar (){
    local++
    return local
  }
  return bar
}).call()

bar()