JavaScript 高级进阶

54 阅读12分钟

1.今日目标

  1. instanceof 和 constructor

  2. ==es6 class 和 继承==

  3. 递归

  4. es6新语法

  5. 闭包

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));