吃透JavaScript核心——函数、作用域、预解析、闭包

589 阅读7分钟

1 函数

1.1 什么是函数

函数就是封装了一段可以被重复执行调用的代码块。

目的:让大量代码重复使用

1.2 函数的使用

函数的使用分为:声明函数,调用函数

//声明函数
function 函数名() {
    //函数体
}
//调用函数
函数名();
console.log(typeof 函数名)//function
  • 注:函数不调用,自己不执行
  • 函数如果没有返回值,则默认为return undefined
    //默认参数语法
    function addWithDefault1(a = 0, b = 0) {//不传参数或者传入为undefined的情况下赋值为0
        return a + b;
    }
    
  • 函数的作用域包含了,函数的参数和函数里的块级作用域。 函数的封装
  • 把一个或者多个功能通过函数的方式封装起来,对外只提供一个简单的函数接口。

1.3 函数的参数

形参和实参

function 函数名(形参1,形参2...) {
    
}
函数名(实参1,实参2...);
  • 在声明函数的()里是实参。
  • 在函数调用的()是形参。
  • 形参是接收实参的,实参是传递给形参的,形参可以看做一个不用声明的变量。

形参和实参的匹配

  • js非常容错:传参数、不传参数、传的参数多、传的参数少。
  • 如果实参个数多于形参的个数, 会取到形参的个数。
  • 如果实参个数少于形参的个数, 多出来的形参定义为undefined🚛

1.4 函数的返回值 return 语句

函数将值返回给调用者。

function 函数名() {
    return 需要返回的结果;
    //只要函数遇到return 就把后边的结果返回给函数的调用者 函数名() = return后面的结果 相当于一个赋值的操作。
}
  • 在实际开发中,经常用一个变量来接受函数的返回结果,使用更简单。
  • return 语句之后的代码不执行
  • return 只能返回一个值。如果用逗号隔开多个值,以最后一个为准。可以用数组来实现返回多个值。
  • 函数如果没有 return,则返回 undefined。

1.5 arguments的使用

所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递过来的所有实参。只有函数才有 arguments 对象。 arguments 展示形式是一个伪数组,有以下特点:

  • 具有数组 length 属性
  • 按照索引的方式进行存储
  • 没有真正数组的一些方法 pop() push()

1.6 函数案例

利用函数计算1-100之间的累加和

//一次声明,多次调用
//1.声明函数
function getSum() {
    var sum = 0;
    for (var i = 1; i <= 100; i++) {
        sum += i;
    }
    console.log(sum);
}
//2.调用函数
getSum();

1.7 函数声明方式

利用函数关键字自定义函数(命名函数)

function fn() {

}
fn();

函数表达式(匿名函数)

//var 变量名 = function() {};
var fun = function() {

}
fun();
// console.log(minius1(10, 2)); // ❌没有定义提升 不能这样写在前边
//函数表达式,可选的名字(也可以把 minius2 省略不写)
const minius1 = function minius2(a, b) {
    return a - b;
};
console.log(minius1(10, 2));

箭头函数

//箭头函数
const minius4 = (a, b) => {
    return a - b;
}

// 如果函数体只是一个表达式,可以把大括号和 return 省略掉
const minius5 = (a, b) => a - b;

//只有一个参数,可以小括号省略掉
const returnSelf = x => x;

2 作用域

2.1 什么是作用域

JS的作用域:变量在某个范围内起作用和效果,目的是为了提高程序的可靠性,减少命名冲突。
作用域:一种存储变量、读取变量的规则。 JS的作用域(ES6 之前,没有块级作用域)可以分为:

  • 全局作用域:整个 script 标签,或者是一个单独的 js 文件。
  • 局部作用域:在函数内部就是局部作用域。变量名只在函数内部起作用。

2.2 变量的作用域

变量分类(根据作用域的不同)

  • 全局变量:在全局作用域下的变量。如果在函数内部没有声明直接赋值的变量,也是全局变量。
  • 局部变量:在局部作用域下的变量。在函数内部的变量。函数的形参也可以看做局部变量。

执行效率

  • 全局变量只有浏览器关闭的时候才会销毁,比较占内存资源。
  • 局部变量当程序执行完毕就会被销毁,比较节约内存资源。

块级作用域

(es6之前)JS没有块级作用域:

  • 块级作用域:{} if {} for {}
  • js在es6的时候新增了块级作用域

在函数内部声明了一个 a 变量,在函数内部可以访问到,在函数外部是访问不到的,这就是函数级作用域的作用

if (3>5) {
   var num = 10;
}
console.log(num);//undefined

2.3 作用域链 变量查找规则

作用域链:内部函数可以访问外部函数的变量,采取的是链式查找的方式来决定取哪个值(就近原则),这种结构我们称为作用域链。

变量查找规则:如果在当前作用域中没有发现此变量的声明,程序就会去他父作用域去查找,直到找到为止,在浏览器中最外层的作用域是 window,如果最后在 window 上都没有找到的话,就会返回 xxx is not defined 查找结束。

3 预解析 变量提升

//例1.
console.log(num);//Uncaught ReferenceError: num is not defined
//例2.
console.log(num);//undefined 坑🚗
var num = 10;
//例3.
fn();//11
function fn() {
    console.log(11);
}
//例4.
fn();//Uncaught TypeError: fun is not a function 坑🚗
var fun = function() {
    console.log(22);
}

3.1 预解析

JS代码是由浏览器中的,JS引擎运行js分为两步:预解析和代码执行。

预解析:Js 引擎会把 js 里面所有的 var 和 function 提升到当前作用域的最前边。

代码执行:安装代码书写的顺序从上往下执行。

3.2 变量预解析和函数预解析

预解析分为两部分:

  • 变量预解析(变量提升):把所有的变量声明提升到当前作用域的最前边。不提升赋值操作。(例2 例4)
  • 函数预解析(函数提升):把所有的函数声明提升到当前作用域的最前边。(例3) 调用必须写在函数表达式的下面

3.3 预解析案例

案例1

var num = 10;
fun();
function fun() {
    console.log(num);
    var num=20;
}

预解析后:

var num;
function fun() {
    var num;
    console.log(num);//undefined
    num=20;
}
num = 10;
fun();

案例2

var num = 10;
function fun() {
    console.log(num);
    var num=20;
    console.log(num);
}
fn();

预解析后:

var num;
function fun() {
    var num;
    console.log(num);//undefined
    num=20;
    console.log(num);//20
}
num = 10;
fn();

案例3

f1();
console.log(a);
console.log(b);
console.log(c);
function f1() {
    var a = b = c = 9;//相当于 var a=9;b=9;c=9;b和c直接复制,没有var声明,当全局变量看。
    //与集体声明不同:var a=9,b=9,c=9;
    console.log(a);
    console.log(b);
    console.log(c);
}

预解析后:

function f1() {
    var a;
    a = b = c = 9;
    console.log(a);//9
    console.log(b);//9
    console.log(c);//9
}
f1();
console.log(a);//error
console.log(b);//9
console.log(c);//9

4 函数作为值

4.1 函数作为参数

function add(a, b) {
    return a + b;
}
function binaryOperator(operand1, operand2, func) {
    const res = func(operand1, operand2);
    console.log(res);
    return res;
}
binaryOperator(2, 5, '+');
binaryOperator(2, 5, '*');
binaryOperator(2, 5, '/');
binaryOperator(2, 5, add);
binaryOperator(2, 5, (a, b) => a * b);
binaryOperator(2, 5, (a, b) => a / b);

4.2 闭包 函数作为返回值

闭包的定义 函数作为返回值

闭包:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

  • 因为作用域的问题,函数内部的变量只能在函数内部使用,外部是访问不了的。:
    function demo() {
      var a = "Tina";
    }
    console.log(a); // //a is not defined
    
  • 闭包要干的事就是可以让外部访问到这个变量,如下所示:
    function demo() {
      var a = "Tina";
      return function () {//🎈🎈🎈函数作为返回值
        return a; //在 `demo()` 中返回一个函数,在返回的函数中再返回这个变量。
      };
    }
    const d = demo();
    console.log(d()); // Tina
    
    • 当我们在外部去调用这个返回出来的函数时就可以得到这个变量的值。
    • d 函数 保存了对 a 的引用,这就形成了闭包。

利用闭包解决实际问题

for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
  • 结果分析: 🧨🧨🧨打印了 10 次 10,这是因为ES6之前,JavaScript 没有块级作用域所致。
    1. 代码执行 for 循环,i 依次从 0 加到 9,循环十次。
    2. 代码等待定时器 1 秒钟时间到,执行定时的里面的内容。
    3. 执行打印 i 语句,因为定时器函数中没有声明 i 变量,所以代码只能去定时器函数外的作用域去查找。
    4. 在外部找到了 i 此时 i 已经变成了 10,所以打印 10 次 10。
  • 利用闭包改写,使之能打印出来 0 ~ 10。
    for (var i = 0; i < 10; i++) {
      (function (i) {
        setTimeout(function () {
          console.log(i);
        }, 1000);
      })(i);
    }
    
    • 在 setTimeout 外面套了一层自执行函数,把每次循环的 i 的结果给保存在当前作用域下,当执行定时器的时候,就可以去当前的作用域去找 i 的值了。
  • 利用let改写 (/const/function)会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于let的块作用特征即可解决。
    for (let i = 0; i < 10; i++) {
      setTimeout(function () {
        console.log(i);
      }, 1000);
    }
    

闭包案例

const buildRepeatCharWithLog = char => {
    let count = 0;
    return num => {
        count++;
        console.log('第' + count + '次调用,重复字符为:' + char)
        let res = '';
        while(num--) {
            res += char;
        }
        return res;
    }
}
const char = '?';
const count = 100;
const repeatBar = buildRepeatCharWithLog('-')
const repeatExclamatory = buildRepeatCharWithLog('!')
// 只有返回的函数可以访问到 char 和 count
//  每个函数会永久“记住”当前函数 **定义** 时用到的作用域
// 函数与其词法环境的引用捆绑在一起构成闭包(closure)
console.log(repeatBar(4));
console.log(repeatExclamatory(4));
repeatBar(4);
repeatBar(4);
repeatExclamatory(4);
repeatExclamatory(4);

image.png