js 高级

394 阅读32分钟

一.JavaScript 进阶

2.知识

其他分支 **三元运算** 其他循环 **对象** **封装** 继承 **原型 原型链 函数** 匿名函数 **箭头函数** 闭包 递归 call和apply **es6**-class  设计模式 冒泡算法

今日目标

  1. 了解Swtich-case分支
  2. 掌握三元表达式简化分支
  3. 了解while循环
  4. 理解冒泡排序
  5. 理解面向对象的概念 思想
  6. 理解创建简单类型和复杂类型的堆栈关系(重点)
  7. 理解创建对象的几种方式
  8. 理解创建对象的优缺点 - 重点!!!
  9. 原型
  10. ==函数的原型链==
  11. 函数和对象的原型链关系
  12. 函数的4种调用方式
  13. 箭头函数
  14. instanceof 和 constructor
  15. ==es6 class 和 继承==
  16. 递归 -- 如果掌握最好
  17. es6新语法 一定要掌握
  18. 闭包

1.基础复习

串讲

  • 变量
    • 变量的作用,语法,命名规则
    • var:可以定义变量,但是会有变量名提升,没有作用域的概念
    • let:没有变量名提升,,要先定义后使用,有作用域的概念
    • const:定义常量(一旦定义赋值之后就不能再修改的变量:以后模块中的成员一般都是const)
  • 数据类型
    • 基本数据类型
      • 也叫值类型
      • Number String,Boolean,undefined,null
    • 复杂数据类型
      • 也叫引用类型,它的本质是一个地址空间
      • 函数,数组,对象
      • 引用类型的空,一般设置为nul
  • 类型转换
    • 转换成字符串
      • toString()
      • "" +
    • 转换成数字
      • parseInt():转换,直到碰到非数值,如果第一个就是非数值,就返回NaN
      • Number():只要有非数值字符串,就返回NaN
      • -0
    • 转换成布尔值
      • js中的false:0,'',"",null,undefined,NaN
      • Boolean(需要转换的变量)
  • 运算符:获取bool值
    • 算术运算符:+ - * / %
    • 赋值运算符:=
    • 比较运算符:
      • > < >= <= == != ===
      • ==:值要相等,它会将字符串转换为数值
      • ===:值和类型也要相等
    • 逻辑运算符:!&& ||
    • 自增自减运算符: ++ --
      • 写在前面:先执行,再使用
      • 写在后面:先使用再执行
    • 运算符的优先级:()
  • 流程控制语句
    • 顺序结构
    • 分支结构:if-else
    • 循环结构:for , forEach(function(value,index){})
  • 数组
    • 创建数组的两种方式
      • let arr = [ ] --语法糖
      • let arr = new Array(...)
    • 下标和长度
      • 只有一个属性:length
    • 取值和存值
      • 通过索引
      • 如果发现操作的数据类型是数组,那么找索引
    • api
      • 增加:push unshift
      • 删除:splice(index,length)
      • 修改:arr[i] = ''
      • 查询(获取): arr[i]
  • 函数
    • 声明和调用
    • 形参和实参
      • 数量对应
      • 顺序对应
      • 类型对应
    • 返回值
      • 如果方法没有返回值,默认会返回undefined
      • 通过return返回值
      • 一个函数中只能有一个return被执行
      • return只能返回一个变量,返回对象更常用
  • 对象
    • 创建对象的方式:{}
    • 属性和方法:特征和行为
    • 存值和取值
      • 通过key进行值的存储和获取
        • obj[key]:如果key是变量,则只能使用[]方式操作
        • obj.key
      • 赋值:
        • 如果key已经存在,就是修改
        • 如果key不存在则是添加--动态特征
    • 对象的遍历
      • for..in >> for(let key in obj) {}
  • 内置对象
    • Math
      • 静态类型
      • 通过Math构造函数直接调用里面的成员
      • 常见:floor ceil random abs
    • Array
    • Date
      • new Date():获取当前日期
      • Date.now():获取当前日期离默认日期的毫秒值
    • String

typeof关键字

typeof操作符返回一个字符串,返回的是操作数的类型

  • typeof 基本类型返回的是字符串值

    • 字符串 》》 string
    • bool 》》boolean
    • 数值 》》number
  • typeof 对象 返回的是object

  • typeof 函数 返回的是function

  • typeof null 返回的object

  • typeof undefined:返回undefined

  • typeof 数组:返回object

逻辑中断

&&:从左到右的顺序进行判断,如果发现某个操作数的逻辑判断是false,那么就不用继续判断了。

||:从左到右的顺序进行判断,如果发现某个操作数的逻辑是true,那么就不用继续判断了。

function fn (n1, n2) {
  n1 = n1 || 0
  n2 = n2 || 0
  console.log(n1 + n2)
}

switch-case分支结构

  • 1.语法
switch(表达式){	// 不是布尔类型:是一个确定的变量
    case1:	 // 值1,值2...都是字面量
        表达式的结果 === 值1,需要执行的代码
        break;
    case2:
        表达式的结果 === 值2,需要执行的代码
        break;
    case3:
        表达式的结果 === 值3,需要执行的代码
        break;
    .......
    default:
        表达式的结果和上面所有的case后面的值都不全等,则会执行这里的代码
        break;
}
  • 2.注意事项
    • 1.表达式的结果要和值一定是全等的关系===
    • 2.break作用:结束该switch语句,所以一般情况下要加上,如果不加上则会发生穿透
      • 穿透:从上一个case代码快执行到下一个case代码快
      • break关键字的作用就是防止穿透
    • 3.default语句可以写在任何地方,也可以省略,但是一般写在最后,这是一种代码规范
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script>
    /**
     switch-case分支结构:常用于值匹配
        * 匹配:全等的关系

     * switch(条件值){
            case 值1:
                条件值 === 值1,需要执行的代码
                break;
           case 值2:
                条件值 === 值2,需要执行的代码
                break;
           case 值3:
                条件值 === 值3,需要执行的代码
                break;
            .......
           default:
                条件值和上面所有的case后面的值都不全等,则会执行这里的代码
                break;
        }

    /**switch语句注意事项
     * 1.表达式的结果要和值一定是全等的关系===
     * 2.break作用:结束该switch语句,所以一般情况下要加上,如果不加上则会发生穿透
     *      * 穿透:从上一个case代码快执行到下一个case代码快
     *      * break关键字的作用就是防止穿透
     *  3.default语句可以写在任何地方,也可以省略,但是一般写在最后,这是一种代码规范
     */

    //示例:用户输入黑马学科编号,告诉用户学习什么学科  1-前端  2-PHP  3-java  4-UI
    let subject= +prompt("请输入您要报名的学科编号,1-前端  2-PHP  3-java  4-UI");

    switch (subject){
        case  1:
            alert("恭喜你选择了2020年最有钱途的学科!");
            break;
        case  2:
            alert("选择了PHP,臭流氓!");
            break;
        case  3:
            alert("选择了Java,请问植发多少钱一根?");
            break;
        case  4:
            alert("未来的UI视觉交互设计师");
            break;
        default :
            alert("脑子有包");
            break;
    }
</script>
</body>
</html>

switch-case穿透用法

  • 合理穿透:多种值需要执行相同代码
<script>
    /**合理穿透:当存在多种值需要执行相同代码时使用穿透可以节省代码
     * 用户输入某一个月份,告诉用户这个月份属于什么季节
     * 12,1,2 冬季
     * 3,4,5 春季
     * 6,7,8 夏季
     * 9,10,11 秋季
     */
    let month = +prompt("请输入月份");
    switch (month){
        case 12:
        case 1:
        case 2:
            alert("冬季");
            break;
        case 3:
        case 4:
        case 5:
            alert("春季");
            break;
        case 6:
        case 7:
        case 8:
            alert("夏季");
            break;
        case 9:
        case 10:
        case 11:
            alert("秋季");
            break;
        default:
            alert("你来自火星吧?");
            break;
    }

</script>

三元表达式

  • 1.运算符根据参与运算的值数量分为一元、二元、三元运算符

    • 一元运算符:只能操作一个值 ++ -- !
    • 二元运算符:操作两个值 1 + 1 1 > 0
    • 三元运算符:操作三个值
  • 2.三元运算符语法

    • 三元运算符: ?:

    • 三元表达式:

      bool表达式?代码1:代码2
      
      • 1.如果表达式成立则执行代码1,否则执行代码2
  • 2.如果代码1或者代码2有运算结果则三元运算式的结果就是他们其中的一个

    • 三元运算符做的事和if-else类似,只是代码更简洁

    • 三元表达式中:表达式部分永远是条件,最终代表整个结果的不是代码1 就是 代码2

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>

<script>
    /*
    一元运算符:由一个值参与的运算符  :  a++    a--  !a
     二元运算符:  由两个值参与的运算符 :  a + b   a > b
     三元(三目)运算符:由三个值参与的运算符
    */

    /*
     * 三元运算符:  ?:
     * 三元表达式:   表达式?代码1:代码2
     *      * 1.如果表达式成立则执行代码1,否则执行代码2
     *      * 2.如果代码1或者代码2有运算结果则三元运算式的结果就是他们其中的一个
     *
     * 三元运算符做的事和if-else类似,只是代码更简洁
     */

    //案例1:
    let num1 = 10;
    num1 > 0 ? console.log('哈哈') : console.log('呵呵');

    //上面这个三元表达式等价于下面的if - else语句
    // if(num1 > 0){
    //     console.log ( "哈哈" );
    // }else{
    //     console.log ( "呵呵" );
    // }

    //案例2:三元表达式一般应用是用来赋值
    let num2 = 20;
    let res2 = num2 > 0 ? num2 + 1 : num2 - 1;
    console.log ( res2 );//21

    //上面这个三元表达式等价于下面的if - else语句
    // if(num2 > 0){
    //     res2 = num2 + 1;
    // }else{
    //     res2 = num2 - 1;
    // }

    //练习:输出性别  (实际开发中,性别通常会使用一个布尔类型来存储,这样存储效率更高)
    let name = "马云";
    let age = 38;
    let gender = true;      //true男 1         false女 0
    console.log("我的名字是"+name+",我的年龄是"+age+",我是一个"+(gender == true ? "男":"女")+"生");


</script>
</html>

三种分支结构语法总结

  • 1.原则上,三种分支结构语句之间可以互转,只不过每一种分支结构语句适用场景不一样
  • 2.if分支结构:适合条件判断
    • 最常用:if-else 两种互斥条件判断
  • 3.switch-case 适合做固定值匹配
  • 4.三元表达式: 比if-else代码更简洁,但是代码量较多时易读性变差。以后它有一个最大的优点:可以在模板中添加
    • 反引号
    • 模板引擎
    • 插值表达式

while循环结构

  • 循环三要素
    • 起始值
    • 条件
    • 变量的变化
  • 1.语法:
while(条件 true/false){
    循环体/需要重复执行的代码;
}
  • 执行步骤:
    • 1.判断条件是否成立
      • 1.1 如果成立,执行循环体代码,然后重复步骤1
      • 1.2 如果不成立,结束循环,执行大括号后面的代码
  • 3.注意点
    • (1)小括号中的语句,无论结果是什么都会转换成布尔类型来判断是否成立
    • (2)避免写一个死循环
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script>

    //需求:打印 3 次 '我爱大前端'

    //复制粘贴弊端:(1)代码冗余  (2)不便于维护
    // console.log ( "我爱大前端" );
    // console.log ( "我爱大前端" );
    // console.log ( "我爱大前端" );

    
    /*
    1.循环结构 : 代码重复执行

    2. 语法
        while(条件 true/false){
            循环体 :需要重复执行的代码
        };

    执行步骤
        1. 判断条件是否成立
            2.1 成立:执行循环体代码。 重复步骤1
            2.2 不成立,循环语句结束,执行大括号后面的代码

    */
    let i = 1;//循环变量,记录循环次数
    while(i<=3){
        console.log ( "我爱大前端" );
       i++;////循环变量自增  自增的目的是为了控制循环的次数,否则这是一个死循环
    }
    console.log('111');//大括号外的代码与循环结构没有关系,还是顺序执行


   //循环语句注意点:
    //循环语句注意点:
    //(1)小括号中的语句,无论结果是什么都会转换成布尔类型来判断是否成立
    //(2)避免写一个死循环
    //let num = 1;
    // while(num < 10){
    //     console.log ( num );
    //     num++;//改变循环变量的值,可以避免死循环
    // }

</script>
</body>
</html>

do-while循环结构

  • 1.语法:
do{
    循环体;
}while( 条件 );
  • 2.执行过程
    • 1.先执行循环体代码
    • 2.执行条件语句
      • 如果结果为true,执行循环体代码
      • 如果为false,循环结束
    • 3.重复步骤2
  • 3.do-while和while实现的循环其实是一样的,只有一个不同点:do-while循环不管怎样先执行一次循环体代码,然后再判断条件
    • while循环:先奏后斩(先判断条件再执行循环体)
    • do-while循环:先斩后奏(不管怎样先执行一次循环体代码,然后再判断条件)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script>

    /* 1. 学习目标 : do-while循环 
       
       2. 学习路线
            (1)复习while语法特点
            (2)学习do-while语法
            (3)介绍do-while语法应用场景
    
    */

    //1.while循环:
    //let i = 1;
    // while(i > 5){
    //     //循环条件不成立,循环体一次都不执行
    //     console.log ( "哈哈哈哈" );
    //     i++
    // }

    //2.do-while循环
    /**
     do-while语法:(用的很少)

     do{
            循环体;
        }while( 条件 );

     特点:无论如何先执行一次循环体,然后再去判断条件
     */
    let i = 1;
        do{
            console.log ( "呵呵呵呵呵" );
            i++;
        }while (i > 5);

    //while循环:先奏后斩(先判断条件再执行循环体)
    //do-while循环:先斩后奏(不管怎样先执行一次循环体代码,然后再判断条件)


    //3.do-while循环与while循环应用场景
    //无论如何需要先执行一次循环体,使用do-while代码更简洁

    //例子:让用户输入账号和密码,如果输入正确就登陆成功,如果输入错误就让他一直输入

    //while循环实现
    // let username = prompt('请输入账号');
    // let password = prompt('请输入密码');
    //
    // while(username != 'admin' || password != '123456'){
    //     username = prompt('请输入账号');
    //     password = prompt('请输入密码');
    // }

    //do-while实现
    do{
        let username = prompt('请输入账号');
        let password = prompt('请输入密码');
    }while(username != 'admin' || password != '123456')

</script>
</body>
</html>

三种循环结构总结

  • 1.原则上,三种循环结构语句之间可以互转,只不过每一种语句的适用场景不一样
  • 2.最常用:for循环:适合循环次数固定
  • 3.while循环:适合循环次数不固定
  • 4.do-while循环:适合循环次数不固定,但是循环体代码至少要执行一次

2-数组排序-冒泡算法

  • 算法algorithm,是一种解决问题的方法
  • 算法的目标:使用最少的内存,最短的时间,解决最多的问题
  • 冒泡算法:
    • 重复地走访过要排序的元素列,依次比较两个相邻的元素
      • 顺序正确:代表位置正确,不需要交换
      • 顺序错误:交换两个元素,让顺序正确
<script>
	/*
		冒泡算法(顺序:从小到大)
		1.从第一个元素开始,比较下一个元素
			* 如果前面一个大于后面的元素:交换
			* 如果前面一个小于或者等于后面的元素:不用动
		2.循环比较数组中的每一个元素:直到最大的那个元素到达数组最后
		
		3.一次循环,只能得出最大的数据排到最后(最前),因此需要根据数组元素的长度来进行循环嵌套
			* 一次只能让当前最大的到最后(如果原来最大的就在最后,那么就是次大的)
			* 根据数组长度实现:每次都能得出一个最大,直到全部都排好序
	*/
    // 定义一个无序数组
    let arr = [3,5,1,8,6,2];
    
    // 外部循环:决定里面循环的次数
    for(let i = 0;i < arr.length;i++){
        // 内部循环:决定当前最大的元素跑到正确的位置去
        for(let j = 0;j < arr.length - 1;j++){
            // j < arr.length - 1 是因为需要进行向后一个进行元素匹配
            
            // 判定当前元素与后一个元素的关系:前面大于后面:交换(其他情况不用变)
            if(arr[j] > arr[j+1]){
                // 交换两个元素的值:采用第三个变量
                let temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
    
    console.log(arr);		// [1,2,3,5,6,8]
</script>

3.面向对象编程

基本概念

所谓面向对象就是指:你想做一件事情,你自己做不到,那么就去找到能够实现这个功能的对象,调用它的方法(传递参数,遵守方法的规则)

什么是对象?

Everything is object (万物皆对象)

对象到底是什么,我们可以从两次层次来理解。

(1) 对象是具体事物的抽象。

一本书、一辆汽车、一个人都可以是对象,当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

问: 书是对象吗

(2)对象是无序键值对的集合,其属性可以包含基本值、对象或者函数

每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。

什么是面向对象?

面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

面向对象与面向过程:

  • 面向过程就是亲历亲为,事无巨细,有条不紊,面向过程是解决问题的一种思维方式,(执行者的角度)
    • 关注点在于解决问题的过程(先xx,然后xx,在xx);
  • 面向对象就是找一个对象,让她去做这件事情(指挥者的角度)
    • 关注点在找到能解决问题的对象上。
  • 面向对象不是面向过程的替代,而是面向过程的封装
  • 例如洗衣服(面向过程和面向对象的区别)

面向对象的特性:

  • 封装性
    • 将功能的具体实现,全部封装到对象的内部,外界使用对象时,只需要关注对象提供的方法如何使用,而不需要关心对象的内部具体实现,这就是封装。
  • 继承性
    • 在js中,继承的概念很简单,一个对象没有的一些属性和方法,另外一个对象有,拿过来用,就实现了继承。
    • 注意:在其他语言里面,继承是类与类之间的关系,在js中,是对象与对象之间的关系。
  • [多态性]
    • 多态是在强类型的语言中才有的。js是弱类型语言,所以JS不支持多态

4.栈和堆

(stack)中主要存放一些基本类型的变量和对象的引用其优势是存取速度比堆要快,但缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性,

(heap 多)用于复杂数据类型(引用类型)分配空间,例如数组对象、object对象;它是运行时动态分配内存的,因此存取速度较慢。

栈和堆的图例

  • 值类型:简单类型,变量在存储的时候,存储的是值本身。如果做为参数传递,仅仅是将栈空间中存储的内容复制一份进行赋值,修改其中一个变量另外不会变化

  • 引用类型:复杂类型,变量在存储的时候,存储的是对象的地址,如果做为参数传递,是将栈空间中存储的引用地址复制一份进行赋值,造成实参和形参指向同一个空间,修改其中一个另外一个也会变化

5.创建对象的几种方式

内置构造函数创建

我们可以直接通过 new Object() 创建:

//在js中,对象有动态特性,可以随时的给一个对象增加属性或者删除属性。
var person = new Object()
person.name = 'Jack'
person.age = 18

person.sayName = function () {
  console.log(this.name)
}

缺点:麻烦,每个属性都需要添加。

对象字面量创建

var person = {
  name: 'Jack',
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}

缺点:如果要批量生成多个对象,应该怎么办?代码很冗余

简单改进:工厂函数

我们可以写一个函数,解决代码重复问题:

function createPerson (name, age) {
  return {
    name: name,
    age: age,
    sayName: function () {
      console.log(this.name)
    }
  }
}

然后生成实例对象:

var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)

缺点:但却没有解决对象识别的问题,创建出来的对象都是Object类型的。

继续改进:构造函数

构造函数是一个函数,用于实例化对象,需要配合new操作符使用。

function Person (name, age) {
  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }
}

var p1 = new Person('Jack', 18)
p1.sayName() // => Jack

var p2 = new Person('Mike', 23)
p2.sayName() // => Mike

而要创建 Person 实例,则必须使用 new 操作符。 以这种方式调用构造函数会经历以下 4 个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  3. 执行构造函数中的代码
  4. 返回新对象

构造函数需要配合new操作符使用才有意义,构造函数首字母一般为大写

构造函数的缺点

使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.say = function () {
    console.log('hello ' + this.name)
  }
}

var person1 = new Person('lpz', 18)
var person2 = new Person('Jack', 16)
console.log(person1.say === person2.say) // => false

解决方案:

图示

解决:提取同一个 say 方法

  1. 解决了浪费内存的弊端
  2. 但是造成了 污染全局变量 的问题
    // 提前将say 声明好
    function say() {
      console.log(this.name);
    }
    function createStudent(name, age) {
      this.name = name;
      this.age = age;
      this.say = say
    }

    const obj = new createStudent("悟能", 83);
    const obj1 = new createStudent("悟能1", 84);

    console.log(obj.say === obj1.say); // true 

图示

6.定义函数的三种方式

  1. 函数声明
  2. 函数表达式
  3. 构造函数Function

函数声明

常规的方式

fn();//函数声明可以先调用,在声明
function fn(参数..){
  console.log("这是函数声明")
  return 返回值
}

函数表达式

const fn = function() {
  console.log("这是函数表达式");  
}
fn();//函数表达式必须先声明,再调用

构造函数Function

所有函数都可以通过Function构造函数来创建

函数也可以看成对象

new Function([前面是参数列表,]最后一个参数是函数体)
var fn1 = new Function("a1", "a2", "alert(a1+a2)");
fn1(1,2);

2.原型 prototype -重点

原型上存放函数

  1. 解决了同一个 say 浪费 内存的问题
  2. 解决了污染全局变量的问题
    function createStudent(name, age) {
      this.name = name;
      this.age = age;
    }
    // 将刚才的全局函数say 直接挂载到 构造函数的原型上 即可
    // prototype 是个对象 每一个构造函数都会内置有的. 我们称之为原型
    createStudent.prototype.say = function () {
      console.log(this.name);
    }

    const obj = new createStudent("悟能", 83);
    const obj1 = new createStudent("悟能1", 84);

    console.log(obj.say === obj1.say); // true 

原型解释

  • 原型的单词是 prototype, 原型的这个名字是行业内共同认可的名字。
  • 原型本质是一个对象,理解为 JavaScript 自动帮我们添加的,只要是构造函数,系统会默认的为构造函数关联一个对象,这个对象就称为构造函数的原型,写在原型中的成员,可以被构造函数所创建的实例调用
  • 原型是 JavaScript 自动帮我们在定义构造函数的时候添加的
  • 所有构造函数的实例,共享一个原型
  • 原型上一般是挂载函数

图示

3.原型 proto

  1. Javascript 规定,每一个(构造)函数都有一个 prototype 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上,以便让同一类型的对象共享方法或其它成员

  2. 实例的 __proto__ 属性 等于 构造函数的 prototype

    function Person (name, age) {
      this.name = name
      this.age = age
    }
    
    console.log(Person.prototype)
    
    Person.prototype.type = 'human'
    
    Person.prototype.sayName = function () {
      console.log(this.name)
    }
    
    var p1 = new Person(...)
    var p2 = new Person(...)
    
    console.log(p1.sayName === p2.sayName) // => true
    
    • 这时所有实例的 type 属性和 sayName() 方法,其实都是同一个内存地址
  3. 注意:由于不同浏览器的兼容性问题,我们使用的时候,都只会使用 构造函数的prototype

  4. 实例的 _proto_ 只是为了方便我们开发的时候查看数据,是不会手动修改和操作它的。

4.原型的关系

构造函数、实例、原型三者之间的关系

构造函数:构造函数就是一个函数,配合new可以新建对象。

实例:通过构造函数实例化出来的对象我们把它叫做构造函数的实例。一个构造函数可以有很多实例。

原型:每一个构造函数都有一个属性prototype,函数的prototype属性值就是原型。通过构造函数创建出来的实例能够直接使用原型上的属性和方法。

所有的构造函数都是Function的实例

Array 和 Person 和 Date 等都是 Function的实例

Function 和 Object的关系

有人说 JavaScript 是作者花了7天时间写出来的产物 - 不完美

console.log(Object.prototype===Function.prototype.__proto__)

Object的顶端呢?

接近顶峰了

 console.log(Object.prototype.__proto__ === null);

5.原型链

1概念

任何一个对象,都有原型对象,原型对象本身又是一个对象,所以原型对象也有自己的原型对象,这样一环扣一环就形成了一个链式结构,我们把这个链式结构称为:原型链。

  • 总结:Object.prototype是原型链的尽头,Object.prototype的原型是null。

属性查找原则

如果是获取操作

  1. 会先在自身上查找,如果没有
  2. 则根据__proto__对应的原型去找,如果没有
  3. 一直找到Object.prototype,如果没有,那就找不到从而报错

6.es5 原型链继承

利用代码的能力实现 面向对象的特性 封装继承

初体验

  1. 子类strudent 继承了父类 Person的属性
// 父类
function Person(name, height) {
    this.name = name;
    this.height = height;
}

Person.prototype.say = function () {
    console.log(this.name);
    console.log(this.height);
}

// 子类
function Student(grade, name, height) {
    // 借用了父类的构造函数,完成对自己的赋值
    Person.call(this, name, height)
    this.grade = grade;
}

// 赋值了父类原型上的所有的 属性和方法
Student.prototype = Person.prototype;
// 修改之类的指向
Student.prototype.constructor = Student;

// 创建子类的实例
const stu = new Student("一年", "周星星", 170);
stu.say();

案例:需求

  1. 有一个负责创建元素的构造函数 A
  2. 有一个负责创建图片的构造函数 B
  3. 构造函数 B 可以使用 构造函数 A 的原型上的所有的功能 实现继承

效果

代码

    // 1 负责创建元素  
    function Element(nodeName, text) {
      const node = document.createElement(nodeName);
      node.classList.add("element")
      node.innerText = text;
      this.node = node;
    }

    // 2 原型上挂载 共用的方法
    Element.prototype.appendTo = function (selector) {
      const parent = document.querySelector(selector);
      parent.appendChild(this.node);
    }

    // 3 创建一个实例
    const div = new Element("div", "做人开心就好");
    // 4 追加到父元素上
    div.appendTo("section");

    // 5 一个新的构造函数 用来创建图片
    function ElementImg(src) {
      // 6 借用了 1 中的构造函数,并且把参数传递了进去
      Element.call(this, "img", "");
      // 7 图片设置路径
      this.node.src = src;
    }

    // 8 继承了 父亲的构造函数上的原型上的所有 函数
    ElementImg.prototype=Element.prototype;			// 修改原型,也就修改了构造函数
    // 9 重新将 constructor 的指向改回来
    ElementImg.prototype.constructor=ElementImg;

    // 10 创建一个图片实例:注意,实例化代码10不能在8和9之前
    const img = new ElementImg("images/01.png");
    
    // 11 创建到父元素上
    img.appendTo("section");

7.作用域及作用域链

let所创建的变量的作用域是从创建这个let变量到它所在的结构的}结束

作用域:变量起作用的区域,也就是说:变量定义后,可以在哪个范围内使用该变量。

var num = 11;//全局变量
function fn(){
  var num1 = 22;//局部变量,只有方法体内可以使用
  console.log(num);  // 函数内部可以使用函数外部声明的变量,全局变量在任何地方都能访问到
  console.log(num1);  
}
console.log(num);

在js里只有全局作用域和函数作用域。

函数作用域是在函数定义的时候作用域就确定下来了,和函数在哪调用无关。

  • 我们只关注函数的定义位置而不关注函数的调用位置
    • 定义函数,函数的作用域就确定了
    • 以后函数做为参数,不影响函数的作用域
var num = 123;
function f1() {
  console.log(num); // 123
}

function f2(){
  var num = 456;
  f1();
}
f2();//打印啥?

作用域链

作用域链:只要是函数,就会形成一个作用域,如果这个函数被嵌套在其他函数中,那么外部函数也有自己的作用域,这个一直往上到全局环境,就形成了一个作用域链。

变量的搜索原则

  1. 从当前作用域开始查找是否声明了该变量,如果存在,那么就直接返回这个变量的值。
  2. 如果不存在,就会往上一层作用域查询,如果存在,就返回。
  3. 如果不存在,一直查询到全局作用域,如果存在,就返回。如果在全局中也没有找到该变量会报错

作用域链练习

// 1 
var num = 10;
fn1();
function fn1() {
  console.log(num);  // ?
  var num = 20;
  console.log(num);  // ?
}
console.log(num);    // ?


// 2 -- 改造上面的面试题
var num = 10;
fn1();
function fn1() {
  console.log(num);  // ?
  num = 20;
  console.log(num);  // ?
}
console.log(num);    // ?


// 3
var num = 123
function f1(num) {
    console.log(num) // ?
}

function f2() {
    var num = 456
    f1(num)
}
f2()


// 4
var num1 = 10;
var num2 = 20;
function fn(num1) {
  num1 = 100;
  num2 = 200;
  num3 = 300;
  console.log(num1);
  console.log(num2);
  console.log(num3);
  var num3;
}
fn();
console.log(num1);
console.log(num2);
console.log(num3);

8.this与函数的四种调用模式

根据函数内部this的指向不同,可以将函数的调用模式分成4种

  1. 函数调用模式
  2. 方法调用模式
  3. 构造函数调用模式
  4. 上下文调用模式(借用方法模式)

函数调用模式

如果一个函数不是一个对象的属性时,就是被当做一个函数来进行调用的。此时this指向了window

function fn(){
  console.log(this);// 指向window 
}
fn();

方法调用模式

当一个函数被保存为对象的一个属性时,我们称之为一个方法。当一个方法被调用时,this被绑定到当前对象

const obj = {
  sayHi:function(){
    console.log(this);//在方法调用模式中,this指向调用当前方法的对象。
  }
}
obj.sayHi();

构造函数调用模式

如果函数是通过new关键字进行调用的,此时this被绑定到创建出来的新对象上。

function Person(){
  console.log(this);
}
Person();//this指向什么?
var p = new Person();//this指向什么?

几个小习题

// 分析思路:1. 看this是哪个函数的  2. 看这个函数是怎么调用的,处于什么调用模式

// 1
var age = 38;
var obj = {
    age: 18,
    getAge: function () {
        console.log(this.age);
    }
}

var f = obj.getAge;
f();//???


// 2
var age = 38;
var obj = {
  age:18,
  getAge:function () {
    console.log(this.age);//???
    function foo(){
      console.log(this.age);//????
    }
    foo();
  }
}
obj.getAge();


// 3
var length = 10

function fn() {
    console.log(this.length)
}
var obj = {
    length: 5,
    method: function (fn) {
        fn() 
        arguments[0]();
    }
}
obj.method(fn, 10, 5);

方法借用模式

也叫上下文模式,分为 apply 与 call,bind

call

call方法可以调用一个函数,并且可以指定这个函数的this指向

const RichWumon = {
    name: "富婆",
    say: function () {
        console.log(this.name, " 我要重金求子");
    }
}

const obj = {
    name: "屌丝"
}

RichWumon.say();			// 富婆
RichWumon.say.call(obj);	// 屌丝

call应用

  1. 将伪数组转成数组
let divs = document.querySelectorAll('div'); // 伪数组
// let divs = document.body.children;
console.log(divs);

function change(nodelist) {
    console.log(Object.prototype.toString.call(nodelist));
    return Array.prototype.slice.call(nodelist);

}

apply

就是apply()方法接受的是一个包含多个参数的数组。而call()方法接受的是若干个参数的列表

可以利用apply 将 刚才的call 的代码修改一下

const RichWumon = {
    name: "富婆",
    say: function () {
        console.log(this.name, " 我要重金求子");
    }
}

const obj = {
    name: "屌丝"
}

RichWumon.say();			// 富婆
RichWumon.say.apply(obj);	// 屌丝

apply应用

1.简化log方法

// 简化log方法
function log() {
    // 不需要改变this
    console.log.apply(console, arguments);
}

bind方法

**bind()**方法创建一个新的函数, 可以绑定新的函数的this指向

var name = '张三';
function Fn(){
    this.age = 1;
    
    console.log(this.name + this.age);
}

Fn();			// 张三 1

// 返回值:新的函数
// 参数:新函数的this指向,当绑定了新函数的this指向后,无论使用何种调用模式,this都不会改变。
let obj = {
    name:'小强',
}
const newFn = Fn.bind(obj);
newFn();		// 小强 1

this的指向

  • 单独使用,this 指向全局对象

    console.log(this); 
    
  • 函数中的 this 指向全局对象

    function show(){
        console.log(this); 
    }
    
    show();
    
  • 在函数内部,this 的指向在函数定义的时候是不能确定的,只有函数执行的时候才能确定

    const a = 18;
    const obj = {
        a: 19,
        b: {
            a: 20,
            c: function () {
                console.log(this.a); 	// 20
            }
        }
    }
    obj.b.c();
    
  • 在方法中,this 指代该调用方法的对象

    const obj ={
    	name:"小白",
    	say:function(){
    		console.log(this);		
    	}
    }
    obj.say()
    

9.箭头函数

格式--定义语法

// 箭头函数是匿名函数,一般做为参数传递
// let test = function (a,b){
//     let sum = a + b 
//     return sum
// }
// let test = (参数) => {函数体}
// 几个小细节
// 1.如果函数体只有一句,那么可以省略{},同时默认会返回函数体的结果,不能写return
// 2.如果只有一个参数,那么可以省略()
// 3.如果没有参数,()也不能省略
// let test = (a,b) =>  a + b 
let test = a =>  a + 10 

let res = test(100)
console.log(res)

特性

// 箭头函数的this是确定的,况且永远不变
// 箭头函数中的this指向  创建这个箭头函数所在对象  的上下文
let obj = {
    name: 'jack',
    say: function () {
        return () => {
            console.log(this) // obj
        }
    }
}
let fn = obj.say()
fn() // obj

let newobj = {}
newobj.fun = fn
newobj.fun() // obj

let rose = {
    name: 'rose'
}

fn.call(rose) // obj

3.instanceof 和 constructor

判断一个实例是否属于某个构造函数

// 构造函数
function Person() {


}
const p1 = new Person();

console.log(p1 instanceof Person);
console.log(p1.__proto__.constructor === Person);

4.es6 class

JavaScript 语言中,生成实例对象的传统方法是通过构造函数,es6的class 的出现 基本上可以替代了es5的构造函数和原型,使之代码结构上更加简洁。
1.构造函数的写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习js的程序员感到困惑。
2.ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
3.新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

关键字

1. class
2. 属性
3. 方法
4. 继承 extends
5. 构造函数 constructor
6. 方法重写 override:子类方法覆盖父类,super.父类方法()
7. 父类的构造函数 super :子类有构造方法且使用this前,必须使用super()

ES5 创建对象

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHi = function () {
  console.log("hello, 我是" + this.name);
};

var p = new Person("lw", 36);
p.sayHi();

ES6 创建对象

class Person {
  constructor(name, age) {
    this.name = name;
  	this.age = age;
  }

  sayHi() {
 	console.log("hello, 我是" + this.name);
  };
}

var p = new Person("lw", 36);
p.sayHi();

ES6-class实现继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

# 继承Person类中的sayHi方法
class Person {
    sayHi(){
        console.log("hello");
    }
}

class Chinese extends Person {}


# 继承Person类中的属性和方法
class Person {
    constructor(name, age, gender){
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    sayHi(){
        console.log("hello");
    }
}

// 子类
class Chinese extends Person {
    constructor(name, age, gender, skin){
        // 在子类中,必须在constructor函数中,首先调用super()
        super(name, age, gender);
        // 调用super之后才可以去写其他代码
        this.skin = skin;
    }
}
var xm = new Chinese("xm", 20, "male", "黄");
console.log(xm);
xm.sayHi();

案例:Tab栏切换

<!DOCTYPE html>
<html>

<head lang="en">
    <meta charset="UTF-8">
    <title>tab栏切换</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        ul {
            list-style: none;
        }

        .box {
            width: 400px;
            height: 300px;
            border: 1px solid #ccc;
            margin: 100px auto;
        }

        .hd {
            height: 45px;
        }

        .hd span {
            display: inline-block;
            /*将行内元素转换成行内块元素,宽高才起作用*/
            width: 90px;
            background-color: pink;
            line-height: 45px;
            text-align: center;
            cursor: pointer;
        }

        .hd span.current {
            /*交集选择器,标签指定式选择器*/
            background-color: purple;
            /*紫色*/
        }

        .bd li {
            height: 255px;
            background-color: purple;
            display: none;
            /*设置隐藏*/
        }

        .bd li.current {
            display: block;
            /*显示*/
        }
    </style>
    <script src="js/TabExchange.js"></script>
</head>

<body>
    <div class="box" id="box">
        <div class="hd">
            <span class="current">体育</span>
            <span>娱乐</span>
            <span>新闻</span>
            <span>综合</span>
        </div>
        <div class="bd">
            <ul id="list">
                <li class="current">我是体育模块</li>
                <li>我的娱乐模块</li>
                <li id="li3">我是新闻模块</li>
                <li>我是综合模块</li>
            </ul>
        </div>
    </div>

    <script>
        let tab = new TabExchange('box', 'current');
        console.dir(tab);
    </script>

</body>

</html>
class TabExchange {
    // 构造函数
    constructor(id, className) {
        // 获取id对应的元素:规范,属于最外层
        this.id = document.querySelector(`#${id}`)
        // 规定内部有两层结构:div>span
        this.hd = this.id.querySelectorAll('div>span')
        // 规范内部有两层结构:div>ul>li
        this.bd = this.id.querySelectorAll('div>ul>li')
        // 保存当前要处理的样式类
        this.style = className;

        // 调用自己的tabClick 方法处理点击事件
        this.tabClick();
    }
    // 绑定点击事件
    tabClick() {
        // 因为接下来要绑定事件,事件里面也会出现this,所以先定义变量that来保存当前TabExchange对象的this
        let that = this

        // 循环遍历,绑定事件,并进行事件处理
        this.hd.forEach(function (item, key) {
            // item就是span,key就是span的下标
            item.addEventListener('click', function () {

                // 排他:清空hd全部样式
                that.hd.forEach(function (item1) {
                    item1.classList.remove(that.style)
                })

                // 清空bd的所有样式
                that.bd.forEach(function (item2) {
                    item2.classList.remove(that.style)
                })

                // 给当前事件元素添加样式
                that.hd[key].classList.add(that.style)
                that.bd[key].classList.add(that.style)
            })
        })
    }
}

5.递归函数

递归函数:函数内部直接或者间接的调用自己

递归的要求:

  1. 自己调用自己(直接或者间接)
  2. 要有结束条件(出口)

递归函数主要是化归思想 ,将一个复杂的问题简单化,主要用于解决数学中的一些问题居多。

  • 把要解决的问题,归结为已经解决的问题上。
  • 一定要考虑什么时候结束让函数结束,也就是停止递归(一定要有已知条件)

什么是递归

在程序中,所谓的递归,就是函数自己直接或间接的调用自己。调用自己分两种:

  1. 直接调用自己
  2. 间接调用自己

就递归而言最重要的就是跳出结构,因为跳出了才可以有结果.

化归思想

**化归思想:**将一个问题由难化易,由繁化简,由复杂化简单的过程称为化归,它是转化和归结的简称。

递归思想就是将一个问题转换为一个已解决的问题来实现

假如有一个函数f, 如果它是递归函数的话, 那么也就是说函数体内的问题还是转换为 f的形式.

function f() {
    ... f( ... ) ...
}

常规应用

1, 2, 3, 4, 5, ..., 100 求和

  1. 首先假定递归函数已经写好, 假设是foo. 即foo(100)就是求1100的和
  2. 寻找递推关系. 就是nn-1, 或n-2之间的关系:foo( n ) == n + foo( n - 1 )
var res = foo(100);
var res = foo(99) + 100;
var res = foo(98) + 99 + 100
........................
var res = foo(2) + 3 + 4 + 5 .....+ 98 + 99 + 100
var res = foo(1) + 2 + 3 + 4 + 5 .....+ 98 + 99 + 100
var res = 1 + 2 + 3 + 4 + 5 .....+ 98 + 99 + 100

将递推结构转换为递归体

function foo(n){
    return n + foo( n - 1 );
}

上面就是利用了化归思想:

- 将 求 100 转换为 求 99
- 将 求 99 转换为 求 98
- ......
- ......
- 将求 2 转换为 求 1
- 求 1 结果就是 1
- 即: foo( 1 ) 是 1

练习:

  • 计算1-100之间所有数的和

    // 1, 2, 3, 4, 5, ..., 100 求和
    // 递归思想就是将一个问题转换为一个已解决的问题来实现
    // 1. 100 + (1到99的和)
    // 2. 100 + (99 + (1到98的和))
    // 3. 100 + (99 + (98 + (1到97的和)))
    // .....
    // 99. 100 + (99 + ( 98 + ( 97 + (96 +(95.....(2 + 1))))))
    function getSum(n){ // 5
        if(n == 1){
            return 1
        }
        return n + getSum(n-1)
    }
    console.log(getSum(100))
    
  • 斐波那契数列

    <script>
        /*1 1 2 3 5 8 13 21 ...*/
        // 1.前面两位固定:结束递归的条件,如果第一位或第二们,直接返回1
        // 2.从第三位开始,每一位是前面两位数字的和
        // 独白:
        // 1.如果要求第100位的数字,那么先得到第99位和第98位数字
        // 2.如果要求第99位的数字,那么先得到第98位和第97位数字
        // 3.如果要求第98位的数字,那么先得到第97位和第96位数字
        // ......
        // 97:如果要求第4位的数字,那么先得到第3位和第2位数字
        // 98:如果要求第3位的数字,那么先得到第2位和第1位数字
        // 99.如果要求第2位的数字,直接返回1
        // 100.如果要求第1位的数字,直接返回1
    
    function fibonaci(n){
        if(n == 2 || n == 1){
            return 1 
        }
        return fibonaci(n-1) + fibonaci(n-2)
    }
    console.log(fibonaci(5))
    </script>
    

案例-生成目录结构

根据数据生成目录结构

 <script>
     var data = {
         parent: '递归案例',
         children: [
             {
                 parent: '目录1',
                 children: []
             },
             {
                 parent: '目录2',
                 children: []
             },
             {
                 parent: '目录3',
                 children: [
                     {
                         parent: '子目录1',
                         children: [{
                             parent: '第一层',
                             children: [
                                 {
                                     parent: '第二层',
                                     children: [{
                                         parent: '第三层',
                                         children: [
                                             {
                                                 parent:'第四层',
                                                 children:[]
                                             }
                                         ]
                                     }]
                                 }
                             ]
                         }]
                     },
                     {
                         parent: '子目录2',
                         children: []
                     }
                 ]
             },
             {
                 parent: '目录4',
                 children: [{
                     parent: '第一层',
                     children: []
                 }]
             }
         ]
     }

// 递归的使用场景
// 1.有结束条件
// 2.不同的条件下执行的操作是一样的
// 方法调用方法自身
// obj是你需要进行目录生成的数据源
let spe = '|&nbsp;&nbsp;&nbsp;&nbsp;'
let str = ""
// count=1:方法的默认值,相当于||
// 如果传递了参数就使用你所传递的参数,如果没有传递参数就使用默认值
// 生成目录
let dirStr = '|' + data.parent + '<br>'
// 生成第一级目录,首先得判断它是否有下一级

function createDir(dir, cnt) {
    if (dir.children && dir.children.length > 0) {
        cnt++
        dir.children.forEach(function(first) {
            dirStr +=
                '|&nbsp;&nbsp;&nbsp;&nbsp;'.repeat(cnt) + first.parent + '<br>'
            createDir(first, cnt)
        })
    }
}

createDir(data, 1)
document.write(dirStr)

6.函数参数默认值

定义函数的同时,可以给形参一个默认值

默认值参数一般在参数列表的最后

    // 定义函数的同时,可以给形参一个默认值
    function show(msg = "大家一起快活呀") {
      console.log(msg);
    }

    show();// 打印 大家一起快活呀
    show("搞笑不");// 打印 搞笑不

7.对象简写

在定义对象的时候,如果属性名和变量名一致,那么可以实现简写

const name = "悟空";
const skill = "72变";
const say = function () { }
const obj = {
    name, skill, say
}
console.log(obj);// {name:"悟空",skill:"72变",say:function(){}}

对象的方法也可以简写

const obj = {
    say() {
        console.log(this);
    }
}

8.解构

提供更加方便获取数组中元素或者对象中属性的写法

获取数组中的元素

      const [a, b, c, d] = [1, 2, 3, 4];
      console.log(a, b, c, d);// 1,2,3,4

元素交互顺序

    let a = 1111;
    let b = 2222;
    [b, a] = [a, b];
    console.log(a, b);// 2222 1111 

获取对象中的属性(重点)

const obj = {
    name: "悟空",
    skill: "72变",
    say() { }
}
const { name, skill,say } = obj;
console.log(name, skill,say);// 悟空 72变 function(){}

9.拓展(展开)运算符 || 剩余运算符

通过 ...符号来获取剩下的参数

函数内获取

function show(a, ...all) {		// 只能放最后
    console.log(a);
    console.log(all);
}


show(1);// 1 []
show(1, 2, 3);// 1 [2,3]

数组内获取

const [a, ...rest] = [1, 2, 3, 4, 5];
console.log(a); // 1
console.log(rest);// [2, 3, 4, 5]

对象内获取

const obj={
    name:"悟空",
    skill:"72变",
    say(){}
}

const {name,...others}=obj;
console.log(name); // 悟空
console.log(others); // {skill: "72变", say: ƒ}

10.Set

永远不会有重复元素的对象

可以理解为不重复的数组

const set = new Set([1, 5, 3, 4]);
set.add(5);
set.add(5);
console.log(set);

Set对象转为数组

const set = new Set([1, 5, 3, 4]);
set.add(5);
set.add(5);
console.log(set);

const arr = [...set];// 将set对象转数组
console.log(arr);

11.闭包

闭包的概念

计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数

闭:封闭:函数内部声明的成员默认在函数外部不能使用

包:包裹:函数内部包裹函数,内部所声明的函数就可以访问内部的成员

在JavaScript中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则产生闭包。简单的来说闭包就是,一个具有封闭的对外不公开的, 包裹结构, 或空间.

在JavaScript中函数可以构成闭包. 一般函数是一个代码结构的封闭结构, 即包裹的特性, 同时根据作用域规则, 只允许函数访问外部的数据, 外部无法访问函数内部的数据, 即封闭的对外不公开的特性. 因此说函数可以构成闭包

产生闭包的条件

当内部函数访问了外部函数的变量的时候,就会形成闭包。
// 1.函数内部声明函数--嵌套函数--内部函数
// 2.内部声明的函数使用了外部声明的成员
// 3.返回了内部函数,外面进行接收,通过这个函数使用内部成员

闭包要解决什么问题?

  1. 闭包内的数据不允许外界访问
  2. 要解决的问题就是间接访问该数据

函数就可以构成闭包, 要解决的问题就是访问到函数内部的数据

我们观察下面的函数foo,在foo内部有一个变量num,能否在函数外部访问到这个变量num呢?

function foo () {
    var num = 123;
    return num;
}

var res = foo();
console.log( res ); // => 123

分析:

在上面的代码中,确实可以访问到num这个函数内部的变量。但是能不能多次访问呢?

不能,因为每次访问都得重新调用一次foo函数,每次调用都会重新创建一个num = 123,然后返回。

解决思路

函数内的数据不能直接在函数外被访问,是因为作用域的关系,上级作用域不能直接访问下级作用域中的数据。

但是如果反过来,下级作用域可以直接访问上级作用域中的数据。那么如果在函数foo内定义一个函数,那么在这个内部函数中是可以直接访问foo中的num的。

function foo() {
    var num = Math.random();    
    function func() {
        return num;    
    }
    return func;
}


var f = foo();
// f可以直接访问num,而且多次访问,访问的也是同一个,并不会返回新的num
var res1 = f();
var res2 = f();

闭包的作用

保护私有变量不被修改

如何获得超过一个数据

函数的返回值只能有一个,那按照上面的方法,我们只能对函数内部的一个数据进行操作。怎么操作函数内的多个数据呢?

可以使用对象,代码如下:

function foo () {
    var num1 = Math.random();
    var num2 = Math.random();
    //可以将多个函数包含在一个对象内进行返回,这样就能在函数外部操作当前函数内的多个变量
    return {
        num1: function () {
            return num1;
        },
        num2: function () {
            return num2;
        }
    }
}

如何完成读取一个数据和修改这个数据

前面讲的都是如何去获取函数内部的数据,接下来我们考虑如何修改函数内部的数据。

同样,也是使用内部的函数进行操作。

function foo() {
    var num = Math.random();
    //分别定义get和set函数,使用对象进行返回
    return {
        //get_num负责获取数据
        get_num: function() {    
            return num;
        },
        //set_num负责设置数据
        set_num: function(value) {
            num = value;
        }
    }
}

总结

一般闭包要解决的的问题就是要想办法间接的获得函数内数据的使用权. 那么我们的可以总结出一个基本的使用模型.

  1. 写一个函数, 函数内定义一个新函数, 返回新函数, 用新函数获得函数内的数据
  2. 写一个函数, 函数内定义一个对象, 对象中绑定多个函数( 方法 ), 返回对象, 利用对象的方法访问函数内的数据

斐波那契数列优化

缓存(cache):数据的缓冲区,当要读取数据时,先从缓冲中获取数据,如果找到了,直接获取,如果找不到,重新去请求数据。

计算斐波那契数列,会有很大的性能问题,因为重复的计算了很多次,因此我们可以使用缓存来解决这个性能问题。

初级优化:

使用缓存的基本步骤:

  • 如果要获取数据,先查询缓存,如果有就直接使用
  • 如果没有,就进行计算,并且将计算后的结果放到缓存中,方便下次使用。
//缓存
var arr = [];
var fbi = function (n) {
  if (n == 1 || n == 2) {
    return 1;
  }
  if (arr[n]) {
    return arr[n];
  } else {
    var temp = fbi(n - 1) + fbi(n - 2);
    arr[n] = temp;//存入缓存
    return temp;
  }
}

缺点:既然使用缓存,就需要保证缓存的数据的安全,不能被别人修改,因此,需要使用闭包来实现缓存的私有化。

function outer() {
  //缓存
  var arr = [];

  var fbi = function (n) {
    if (n == 1 || n == 2) {
      return 1;
    }
    if (arr[n]) {
      return arr[n];
    } else {
      var temp = fbi(n - 1) + fbi(n - 2);
      arr[n] = temp;//存入缓存
      return temp;
    }
  }
  return fbi;
}
var fbi = outer();
console.log(fbi(40));