八股文不用背-打通变量->作用域->闭包->函数柯里化 层层递进

220 阅读6分钟

变量

变量的使用分为两个步骤

  1. 变量声明
  2. 变量赋值与取值 比如:
  var a;
  a = 1;

作用域

js代码中有函数、代码块、全局环境三种区域

  • 函数:即function(){}
  • 代码块:{}包裹起来的区域(比如for、if等)
  • 全局环境:即整个js文件中除函数、代码块的区域

以上三种区域需要维护着各自的变量,作用域就是为了这种情况产生的。 他们三者对应的作用域分别为

  • 函数作用域
  • 块级作用域
  • 全局作用域 比如:
// index.js
var a = 1 // 全局作用域
function func(){
    var a = 2 // 函数作用域
}
if (true) {
    let a = 3 // 块级作用域
}

声明提前

请看

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

第一次打印a是什么?

变量的使用分为声明存取,由于js会在编译阶段(在执行阶段之前)将变量的声明移动到该变量对应的作用域顶部,这一过程叫声明提前,也叫变量提升

所以上面的代码会转化成下面的代码然后才执行。

var a = 1
function f(){
    var a
    console.log(a) // undefined a已经被定义了,但还没被赋值 
    a = 2
    console.log(a) // 2
}

var和let

es6之前定义变量使用的是var var具有以下特性:

  • 声明提前
  • 无视块级作用域 es6之后多了两个定义变量的方式:let和const,他们具有一下特性
  • 不会声明提前
  • 会锁定在块级作用域 比如:
console.log(b) // undefined 声明了,但还没赋值
console.log(a) // 报错,还没声明
if(true){
    console.log(b) // undefined
    console.log(a) // 报错
    let a = 1
    var b = 1
}

var和let的区别有更为详细的讲解,各位可以去查找>

作用域链

请看

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

很明显输出1

明明我们是在函数作用域内访问a,而这个作用域内没有a,那为什么能获取到全局作用域里的a呢?

我们将这些作用域用一个对象来理解,js在编译阶段会将这些作用域以对象的形式链接起来形成一个链表。

当js运行到该作用域时,作用域里的变量会注册到对应的作用域对象上。

比如:

let a = 1
function func1(){
    let a = 2
    function func2(){
        let a = 3
    }
}
if(true){
    let a = 4
}

这样形成的链表是:

image.png

作用域链总结(很重要)

  • 作用域链是在编译阶段形成的,非运行阶段
  • 作用域对象里的变量是在运行阶段添加的
  • 当javascript访问某个作用域的某个变量时,如果该变量不在当前作用域对象上时,便会沿着作用域链往上查找

闭包

请看

let a = 1
function func1(){
    let a = 2
    function func2(){
        return a
    }
    return func2()
}
func1() // 2

这似乎对各位看官来说没什么问题。

请看下一个:

let a = 1
function func1(){
    let a = 2
    function func2(){
        return a
    }
    return func2
}
func1()()

func1()()到底返回了什么?


答案是2,而不是1

步骤如下:

  1. func1()返回的是func2这个方法,然后我们func2()

  2. 在func2中我们访问了a,然后func2的作用域对象中并没有a,那么js就会沿着作用域链往上找,那么作用连是怎么样的呢。 image.png

  3. func2作用域对象没有,那就去func1作用域找a,然后a=1,返回值。

上面这个例子就是闭包,而闭包的特性如下:

  • func2能获取到func1中的变量,这样,我们就能在一个作用域中访问到另一个和它没有关联的作用域中的变量;也能实现java中私有变量这种功能。
  • 由于func2会一直存在着对func1中的变量的引用,所以很容易产生内存泄露。

函数柯里化

背景

实现一个方法add,接受三个参数,返回三个参数之和

function add(num1,num2,num3){
    return num1+num2+num3
}
add(1,2,3) //6

很简单。

我们将问题扩展一下,如果想要实现let sum = add(1)(2)(3),这个sum能当做6来使用,比如sum == 6,let seven = sum + 1,这该怎么办呢?
很明显add(num)要返回方法,那我们来试试

function add(num1){
    return function(num2) {
        return function(num3){
            return num1+ num2 + num3
        }
    }
}
add(1)(2)(3) // 6

通过返回方法 + 闭包能保存着上一个函数的变量的特性,我们实现了add(1)(2)(3) == 6。

但如果我们add(1)(2)......(100000),我们总不能写上万个函数返回吧。

如果有一个函数能接受一个参数,并且将这个参数加到一个变量中,然后返回一个函数,那么从代码层面来说只需要写一个递归,那来试试:

function add(num1){
    let sum = num1
    let a = function (num){
        sum+=num
        return a
    }
    return a
}
sum = add(1)(2)(3) // [Function: a]
sum == 6   // false

怎么说呢,这样只能返回函数a,sum == 6 很明显返回的是false,因为sum需要转化为数值类型,所以会调用a.valueOf(),然而这个方法只返回了函数a本身,不是一个基本数据类型,所以转向使用a.toString(),返回'[object function]',这明显不等于6(此处是js类型转换的知识点)。

不过既然sum == 6 最终会调用a.toString(),那么我们重写a的toString方法,然后将add的sum返回来不就可以了吗?(此处是js的隐式调用的知识点)

function add(num1){
    let sum = num1
    let a = function (num){
        sum+=num
        return a
    }
    a.toString = function(){
        return sum
    }
    return a
}
sum = add(1)(2)(3) // [Function: a]
sum == 6   // true
let seven = sum + 1 // 7

好了,实现了,seven就是7了,想要6就只要sum + 0。

我们在吧问题扩展一下,add(1,2,3)(4)(5,6)这种是每次接受的参数个数都不一致,但也没什么难的

function add(){
    let sum = [...arguments].reduce((sum,item)=>{return sum+item},0)
    let a = function (){
        sum+=[...arguments].reduce((sum,item)=>{return sum+item},0)
        return a
    }
    a.toString = function(){
        return sum
    }
    return a
}
sum = add(1,2,3)(4)(5,6) // [Function: a]
sum == 21   // true
let seven = sum + 1 // 22

我们再优化一下,其实add(1,2,3)(4)(5,6),我们只要将所有的参数放一个数组里面,然后toString里面reduce返回即可

function add(){
    let args = [...arguments]
    let a = function () {
        args = args.concat(...arguments)
        return a
    }
    a.toString = function () {
        return args.reduce((sum, item) => {
            return sum += item
        })
    }
    return a
}
sum = add(1,2,3)(4)(5,6) // [Function: a]
sum == 21   // true
let seven = sum + 1 // 22

至此,就是闭包的应用之一-函数柯里化。

审核大大,给个推荐

看到这里的看官,麻烦点个赞赞把