第一部分:JS基础之变量类型和计算

知识点梳理

变量类型
  • 值类型

    值类型的代码演示来分析值类型的本质

    // 值类型
    let a = 100
    let b = a
    a = 200
    console.log(b) // 100
    

    image-20210808174844656

    首先我们明确一点:保存原始值的变量是按值访问的,变量存储在栈空间中,我们操作的就是存储在变量中的值。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置

    常见值类型: undefined、string、number、boolean、symbol

  • typeof 运算符(关联题目:typeof 能判断哪些类型)

    1. 识别所有值类型

      let a
      let str = 'abc'
      let n = 100
      let b = true
      let s = Symbol('s')
      
      console.log(typeof a)	// undefined
      console.log(typeof str) // string
      console.log(typeof n)	// number
      console.log(typeof b)	// boolean
      console.log(typeof s)	// symbol
      
    2. 识别函数

      console.log(typeof function() {})	// function
      
    3. 判断是否是引用类型(不可再细分)

      console.log(typeof null)	// object
      console.log(typeof ['a', 'b'])	// object
      console.log(typeof { x: 100 })	// object
      
  • 引用类型(关联题目:值类型与引用类型的区别)

    引用类型的代码演示来分析引用类型的本质

    // 引用类型
    let a = { age: 20 }
    let b = a
    b.age = 21
    console.log(a.age)	// 21
    

    image-20210808174916123

    引用值是保存在堆内存中的对象。JS 不允许直接访问堆内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是该对象的引用而非实际的对象本身。为此,保存引用值的变量是按引用访问的

    在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,一个变量对对象的修改会在另一个变量中体现出来

    常见值类型: object、array、null、function

深拷贝(面试常考)

首先我们要搞清什么是浅拷贝和深拷贝

浅拷贝:浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

深拷贝:深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

  • 手写深拷贝

    手写深拷贝四点要注意:

    1. 注意判断值类型和引用类型
    2. 注意判断是数组还是对象
    3. 注意判断属性是不是实例本身的属性
    4. 递归。因为属性也有可能是对象或数组
    /**
     * 深拷贝
     * @param {Object} obj 要拷贝的对象
     */
    function deepClone(obj = {}) {
        // obj 是 null, 或者不是对象或数组, 直接返回
        if (typeof obj != 'object' || obj == null) {
            return obj
        }
        
        // 初始化返回结果
        let result
        if (obj instanceOf Array) {
            result = []
        } else {
            result = {}
        }
        
        for (let key in obj) {
            // 保证 key 不是原型的属性
            if (obj.hasOwnProperty(key)) {
                // 递归调用,因为属性也有可能是对象或数组类型
                result[key] = deepClone(obj[key])
            }
        }
        
        // 返回结果
        return result
    }
    
变量计算
  • 类型转换

    三处常发生的类型转换

    1. 字符串拼接

      如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面

      如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起

      const a = 100 + 10	//110
      const b = 100 + '10'	// '10010'
      const c = true + '10'	// 'true10'
      
    2. ==

      100 == '100'	//true
      0 == ''			//true
      0 == false		//true
      false == ''		//true
      null == undefined	// true
      

      既然 == 运算符的类型转换情况那么多,那我们该如何记忆呢?

      // 除 null 之外,其他一律用 ===
      
      const obj = { x: 100 }
      if (obj.a == null) {}
      // 相当于
      if (obj.a === null || obj.a === undefined) {}
      
    3. if 语句和逻辑运算

      truly 变量!!a === true 的变量

      falsely 变量!!a === false 的变量

      // 以下是 falsely 变量.除此之外都是 truly 变量
      !!0 === false
      !!NaN === false
      !!'' === false
      !!null === false
      !!undefined === false
      !!false === false
      

      if 语句判断的就是 truly 变量和 falsely 变量

      // truly 变量
      const a = true
      if (a) {
          // ...
      }
      
      const b = 100
      if (b) {
          // ...
      }
      
      // falsely 变量
      const c = ''
      if (c) {
          // ...
      }
      
      const d = null
      if (d) {
          // ...
      }
      
      let e	// 未初始化,默认为 undefined
      if (e) {
          // ...
      }
      

      逻辑运算

      console.log(10 && 0)	// 0,因为 0 是 falsely 变量,直接返回
      console.log('abc' || '')	// 'abc',因为 'abc' 是 truly 变量,直接返回
      console.log(!window.abc)	// true
      

第二部分:JS基础之原型和原型链

知识点梳理

class 和 继承
  • class 的基本使用

    // 类
    class Student {
        // 通过 constructor 类构造函数来进行实例化
        constructor (name, number) {
            this.name = name
            this.number = number
        }
    
        sayHi () {
            console.log(
                `姓名:${this.name} 学号:${this.number}`
            )
        }
    }
    
    // 通过类 new 对象/实例
    const xialuo = new Student('夏洛', 23)
    xialuo.sayHi()
    
    const madongmei = new Student('马冬梅', 23)
    madongmei.sayHi()
    
  • 继承

    // 父类
    class People {
        constructor (name) {
            this.name = name
        }
    
        eat(){
            console.log(`${this.name} is Eating...`)
        }
    }
    
    // 子类 通过 extends 来继承父类 People
    class Student extends People {
        constructor (name, number) {
            // 通过 super 来调用父类类构造函数,并返回实例
            super(name)
            this.number = number
        }
    
        sayHi () {
            console.log(
                `姓名:${this.name} 学号:${this.number}`
            )
        }
    }
    
    // 子类
    class Teacher extends People {
        constructor (name, major) {
            super(name)
            this.major = major
        }
        teach () {
            console.log(
                `姓名:${this.name} 教授课程:${this.major}`
            )
        }
    }
    
    // 学生实例
    const xialuo = new Student('夏洛', 233)
    xialuo.sayHi()
    xialuo.eat()
    
    // 老师实例
    const wanglaoshi = new Teacher('王老师', '语文')
    wanglaoshi.teach()
    wanglaoshi.eat()
    
    console.log(wanglaoshi)
    
类型判断 instanceof

我们可以通过 instanceof 关键字来判断对象/实例属不属于指定类

xialuo instanceof Student // true
xialuo instanceof People // true
xialuo instanceof object // true

[] instanceof Array // true
[] instanceof Object // true

{} instanceof Object // true
原型和原型链
  • 原型

    // class 实际上是函数,可见时语法糖
    typeof People // 'function'
    typeof Student // 'function'
    
    // 隐式原型和显示原型
    console.log(xiaoluo.__proto__)
    console.log(Student.prototype)
    console.log(xialuo.__proto__ === Student.prototype)
    

    image-20210811175324422

    • 原型关系
      1. 每个 class 都有显示原型 prototype
      2. 每个实例都有隐式原型 __ proto __
      3. 实例的 __ proto __ 指向对应 class 的 prototype
    • 基于原型的执行规则
      1. 获取属性 xialuo.name 或执行方法 xialuo.sayhi() 时
      2. 先在自身属性和方法寻找
      3. 如果找不到则自动去 __ proto __ 中查找
  • 原型链

    console.log(Student.prototype.__proto__)
    console.log(People.prototype)
    console.log(People.prototype === Student.prototype.__proto__)
    

    image-20210811195802980

    每一个对象/实例都有一个 隐式原型 __ proto__ 指向构造它的类的 显式原型 prototype(隐式原型其实就是显式原型的一个指针)。而这个类又是某个某个类的子类,那么这个类的显式原型上有一个隐式原型指向这个类的父类的显式原型。那么这样一种解 构,我们称作原型链

    其实子类继承于父类,子类的原型可以看作是父类的一个实例,但又不完全是(因为没有实例属性),因为只有实例才有隐式原型指向构造它的类的显式原型

    那么一个对象的属性查找,就是沿着这条原型链进行查找的。instanceof 操作符就是检查该对象的原型链上有没有指定类型的原型

手写简易 jQuery 考虑插件和扩展性
class jQuery {
    constructor (selector) {
        const result = document.querySelectorAll(selector)
        const length = result.length
        for (let i = 0; i < length; i++) {
            this[i] = result[i]
        }
        this.length = length
        this.selector = selector
        // 类数组对象
    }

    get (index) {
        return this[index]
    }

    each (fn) {
        for (let i = 0; i < this.length; i++) {
            const elem = this[i]
            fn(elem)
        }
    }

    on (type, fn) {
        return this.each(elem => {
            elem.addEventListener(type, fn, false)
        })
    }

    // 扩展很多 DOM API
}

// 插件 在原型上去扩展方法
jQuery.prototype.dialog = function (info) {
    alert(info)
}

// 造轮子 继承 jQuery 类去扩展自己的方法和属性, 那么以后只要使用这个子类就行了
class myJQuery extends jQuery {
    constructor (selector) {
        super(selector)
    }
    // 扩展自己的方法
    addClass (className) {
        ...
    }
}

第三部分:JS基础之作用域和闭包

知识点梳理

作用域和自由变量
  • 作用域

    image-20210812172222690

    作用域代表了变量合法的使用范围,如果变量在范围之外去使用,则会报错

    作用域分类

    • 全局作用域

    • 函数作用域

    • 块级作用域

      // 块级作用域
      if (true) {
          let x = 100
      }
      
      console.log(x) // 会报错
      
  • 自由变量

    一个变量在当前作用域没有定义,但被使用了

    向上级作用域,一层一层依次寻找,直至找到为止

    如果到全局作用域都没找到,则报错

闭包

何为闭包?如果不死扣概念和底层逻辑,其实是作用域应用的特殊情况,有两种表现:

  1. 函数作为参数被传递
  2. 函数作为返回值被返回

总而言之,只要函数定义的地方和函数执行的地方不一样,都会产生闭包,我们近乎可以把这个函数称作闭包

以下是针对上面两种表现的实际例子:

// 函数作为返回值
function create () {
    let a = 100
    return function () {
        console.log(a)
    }
}

let fn = create()
let a = 200
fn()   // 100
// 函数作为参数被传递
function print (fn) {
    let a = 200
    fn()
}
let a = 100
function fn () {
    console.log(a)
}
print(fn)   // 100

注意,**所有的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方!**这一点在闭包中尤为明显

以上对于闭包的描述并没有太深入,只是引入了闭包常见的使用场景。接下来我们引入作用域链及执行上下文来总结描述以下闭包的基本原理:

  1. 首先,只要我们定义了一个函数,在预编译阶段就会为它创建作用域链,并且预装载包含上下文的活动对象及全局变量对象,并把这个作用域链保存在内部的 [[Scope]] 中
  2. 如若外部函数将内部定义的函数作为函数值返回,它的执行上下文就会从执行上下文栈中弹出,但是它的活动对象却保留了下来,因为内部定义的函数的作用域链上还保留着对它的引用(这时候产生了闭包)
  3. 此时这个被返回的函数被调用,在创建执行上下文阶段,会通过复制函数的 [[Scope]] 来创建作用域链,接着会创建函数的活动对象并将其推入作用链的前端。然后在执行阶段查找变量时,会先从自身活动对象上查找,再根据作用域链,从包含上下文的活动对象上查找,直至全局变量对象

只要在一个函数中定义了一个内部函数,且内部函数使用了外部函数的变量(称之为自由变量),外部函数将这个内部函数作为返回值返回,这种情况,就会产生闭包!

this