Skip to content
本页目录

JS基础知识

主要知识点:

  1. 变量的类型与计算
  2. 原型与原型链
  3. 作用域与闭包
  4. 异步与单线程

1. 变量的类型与计算

题目

  • typeof 能判断哪些类型

  • 何时使用 == 何时使用 ===

  • 值类型和引用类型的区别

    js
    // 值类型和引用类型的区别
    const obj1 = { x: 100, y: 200 }
    const obj2 = obj1
    let x1 = obj1.x
    obj2.x = 101
    x1 = 102
    console.log(obj1) // 这里打印出什么?
    
  • 手写深拷贝

知识点

变量类型

回顾JS的变量类型有哪些?

值类型与引用类型

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

值类型与引用类型有什么区别?

值类型直接定在内存栈中, 引用类型将值定义在内存堆中并通过引用地址表示

哪些是值类型? 哪些是引用类型?

  • 常见值类型

    js
    const a // undefined
    const s = 'abc'
    const n = 100
    const b = true
    const s = Symbol('s')
    
  • 常见引用类型

    js
    const obj = { x: 100 }
    const arr = ['a', 'b', 'c']
    const n = null // 特殊引用类型,指针指向为空地址
    function fn() {} // 特殊引用类型,但不用于存储数据,所以没有“拷贝、复制函数”这一说
    

如何判断类型?

typeof -> 能判断所有值类型、能判断函数、能识别引用类型

js
// 判断所有值类型
let a
console.log(a) // 'undefined'
const str = 'abc'
typeof str  // 'string'
const n = 100
typeof n // 'number'
const b = true
typeof b // 'boolean'
const s = Symbol('s')
typeof s // 'symbol'
js
// 能判断函数
typeof console.log // 'function'
typeof function () {} // 'function'

// 能识别引用类型(不能再继续识别)
typeof null // 'object'
typeof ['a', 'b'] // 'object'
typeof { x: 100 } // 'object'

变量计算

变量计算一般用于值类型,引用类型会通过 API 来修改数据。

  • 数字 加减乘除
  • 字符串 拼接
  • 逻辑运算 && || ! == if-else

这其中,会隐含比较大的坑 —— 强制类型转换

字符串拼接( + 号)

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

== 和 ===

js
// == 会尝试强制类型转换
100 == '100'   // true
0 == ''  // true
0 == false // true
false == '' // true
null == undefined  // true

总之,== 会尝试进行强制类型转换,至于转换的规则大家没必要,只需要记住一点

  • 所有的地方都用 ===
  • 除了判断是 null 或者 undefined 时用 if (obj.a == null) —— 这也是 jQuery 源码中的方式
js
const obj = { x: 100 }
if (obj.a == null) { }
// 相当于:
if (obj.a === null || obj.a === undefined) { }

深拷贝

面试中经常会遇到浅拷贝与深拷贝有什么区别? 重点就是在于一个引用类型的考察以及对类型判断递归的使用

js
// 深拷贝
function deepClone(obj = {}) {
    if (typeof obj !== 'object' || obj == null ) {
        // 不是对象或者数组形式,或是 null ,直接返回
        return obj
    }

    // 初始化返回结果
    let result
    if (obj instanceof Array) {
        result = []
    } else {
        result = {}
    }

    // 变量
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 保证不是原型属性(原型和原型链部分会讲解)

            // 递归调用
            result[key] = deepClone(obj[key])
        }
    }

    // 返回结果
    return result
}

const obj1 = { x: 100, y: 200 }
const obj2 = deepClone(obj1)
obj1.x = 101
console.log(obj2) // x: 100

题目解答

JS中使用typeof能得到的哪些类型

针对这个题目,可以通过以下程序进行验证

js
typeof undefined // 'undefined'
typeof 'abc' // 'string'
typeof 123 // 'number'
typeof true // 'boolean'
typeof Symbol('s') // 'symbol'
typeof {}  // 'object'
typeof [] // 'object'
typeof null // 'object'
typeof console.log // 'function'

何时使用=== 何时使用==

  • 所有的地方都用 ===
  • 除了判断是 null 或者 undefined 时用 if (obj.a == null) —— 这也是 jQuery 源码中的方式
js
const obj = { x: 100 }
if (obj.a == null) { }
// 相当于:
if (obj.a === null || obj.a === undefined) { }

值类型和引用类型的区别

js
// 值类型和引用类型的区别
const obj1 = { x: 100, y: 200 }
const obj2 = obj1
let x1 = obj1.x
obj2.x = 101
x1 = 102
console.log(obj1) // { x: 101, y: 200 }

2. 原型和原型链

JS是基于原型 prototype 继承的语言,故而在以往面试题中原型也是被常问的知识点。

在ES6 可使用类 class 继承(语法糖,本质还是原型继承)

**【重点提示】ES6 已经普及使用,原先构造函数的实现方式已经成为历史。**以讲解 class 为主,不会再从构造函数讲起。

题目

  • 如何准确判断一个变量是数组类型
  • 实现一个简易的 jQuery ,考虑插件和扩展性 —— PS: 虽然 jQuery 已应用不多,但借助学习原型非常好
  • class 是语法糖,其本质是什么?

知识点

class

定义与使用

js
// 声明一个类
class Student {
    constructor(name, number) {
        // 属性
        this.name = name
        this.number = number
    }
    // 方法
    sayHi() {
        console.log(`姓名 ${this.name},学号 ${this.number}`)
    }
}

// 用类来声明对象
let xialuo = new Student('夏洛', 100)
xialuo.sayHi()
let madongmei = new Student('马冬梅', 101)
madongmei.sayHi()

继承

js
// 父类
class People {
    constructor(name) {
        this.name = name
    }
    eat() {
        console.log(`${this.name} eat something`)
    }
}
// 子类
class Student extends People {
    constructor(name, number) {
        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} teach you ${this.major}`)
    }
}

const xialuo = new Student('夏洛', 100)
xialuo.sayHi()
const wanglaoshi = new Teacher('王老师', '语文')
wanglaoshi.teach()

类型判断

引用类型使用 instanceof

js
xialuo instanceof Student // true
xialuo instanceof People // true
xialuo instanceof Object // true

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

{} instanceof Object // true

原理后面再看!!!

原型

通过typeof判断通过class定义的类

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

显示原型和隐式原型的关系(可画图说明!!!)

  • 每个 class 都有 prototype 显示原型
  • 每个实例都有 proto 隐式原型
  • 实例的 proto 指向对应 class 的 prototype

基于原型的执行逻辑

  • 执行实例方法时,如 xiaoluo.sayHi()
  • 会先从实例自身属性查找(可通过 hasOwnProperty 判断)
  • 如果找不到则自动去 proto 查找

原型链

以第二个例子为继承的例子。

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

继续补充原型图示!!!

根据之前的规则,再去演练 xialuo.eat()

原型和原型链综合演练

根据以上规则和图示,综合演练

  • xialuo.name
  • xialuo.sayHi()
  • xialuo.eat()

继续延伸,xialuo.hasOwnProperty 从哪里得来? —— 继续补充原型图示!!!

题目解答

如何准确判断一个变量是数组类型

instanceof

实现一个简易的 jQuery ,考虑插件和扩展性

基本框架

js
class jQuery {
    constructor(selector) {
        const result = document.querySelectorAll(selector)
        const length = result.length
        for (let i = 0; i < length; i++) {
            this[i] = selectorResult[i]
        }
        this.length = length
    }
    get(index) {
        return this[index]
    },
    each(fn) {
        for (let i = 0; i < this.length; i++) {
            const elem = this[i]
            fn(elem)
        }
        return this
    },
    on(type, fn) {
        return this.each(elem => {
            elem.addEventListener(type, fn, false)
        })
    }
}

插件机制

js
// 使用继承 —— 基于 jQuery 基本功能,再造一个更强大的轮子
class myJQuery extends jQuery {
    constructor(selector) {
        super(selector)
    }
    // 扩展自己的方法
    addClass(className) {
    },
    style(data) {
    }
}

// 使用原型 —— 还用 jQuery ,仅仅扩展一个功能而已
jQuery.prototype.dialog = function (info) {
    console.log(this) // this 即 jQuery 对象
}

class 是语法糖,其本质是什么?

  1. 通过typeof判断得知为function
  2. 本质是构造函数、继承等操作都是基于原型去实现的

3. 作用域与闭包

作用域、闭包是一个头疼的玩意,特别对应初次接触者来说真可谓是头疼,作为面试他也是面试题中的常青树。

题目

  • this 的不同应用场景

  • 创建 10 个<a>标签,点击的时候弹出来对应的序号

    js
    // 创建 10 个`<a>`标签,点击的时候弹出来对应的序号
    let i, a
    for (i = 0; i < 10; i++) {
        a = document.createElement('a')
        a.innerHTML = i + '<br>'
        a.addEventListener('click', function (e) {
            e.preventDefault()
            alert(i)
        })
        document.body.appendChild(a)
    }
    
  • 实际开发中闭包的应用

  • 手写 bind 函数

知识点

作用域

所谓作用域,即一个变量的合法使用范围

js
let a = 0
function fn1() {
    let a1 = 100

    function fn2() {
        let a2 = 200

        function fn3() {
            let a3 = 300
            return a + a1 + a2 + a3
        }
        fn3()
    }
    fn2()
}
fn1()

作用域分类

  • 全局作用域:在全局定义的变量,全局都可用,像 document
  • 函数作用域:在某个函数中定义的变量,只能用于当前函数,像 a b
  • 块级作用域(ES6):只能活跃于当前的块,示例如下
js
// ES6 块级作用域
if (true) {
    let x = 100
}
console.log(x)  // 会报错

自由变量

  • 一个变量在该作用域没有被定义,但被使用
  • 向上级作用域去寻找该变量,层层往上找 —— 如上面的示例
js
let a = 0
function fn1() {
    let a1 = 100

    function fn2() {
        let a2 = 200

        function fn3() {
            let a3 = 300
            return a + a1 + a2 + a3
        }
        fn3()
    }
    fn2()
}
fn1()

闭包

闭包 —— 作用域应用的一个特殊情况。一般有两种书写形式:

  • 函数作为参数被传入
  • 函数作为返回值
js
// 函数作为返回值
function create() {
    let a = 100
    return function () {
        console.log(a)
    }
}
let fn = create()
let a = 200
fn()

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

从上面代码中可得出 函数中自由变量在使用时 **函数定义的地方(不是执行的地方)**去寻找上级作用域

这种现象称为闭包

闭包示例

隐藏定义的数据 只提供API

js
function createCache() {
    let data = {}  // 闭包中的数据,被隐藏,不被外界访问
    return {
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}
let c = createCache()
c.set('a', 100)
console.log( c.get('a') )

this

常见场景

  • 作为普通函数调用
  • 使用 call apply bind
  • 作为对象方法调用
  • 在 class 的方法中调用
  • 箭头函数
js
// this 场景题 - 1
function fn1() {
    console.log(this)
}
fn1() // 打印什么

fn1.call({ x: 100 }) // 打印什么

const fn2 = fn1.bind({ x: 200 })
fn2() // 打印什么

// this 场景题 - 2
const zhangsan = {
    name: '张三',
    sayHi() {
        console.log(this)
    },
    wait() {
        setTimeout(function() {
            console.log(this)
        })
    },
    waitAgain() {
        setTimeout(() => {
            console.log(this)
        })
    }
}
zhangsan.sayHi() // 打印什么
zhangsan.wait() // 打印什么
zhangsan.waitAgain() // 打印什么

// this 场景题 - 2
class People {
    constructor(name) {
        this.name = name
        this.age = 20
    }
    sayHi() {
        console.log(this)
    }
}
const zhangsan = new People('张三')
zhangsan.sayHi() // 打印什么

this的指向不用特意去死记硬背 抓准核心点:看调用对象

题目解答

this 的不同应用场景

  • 作为普通函数调用 -> 一般指向函数调用者
  • 使用 call apply bind -> 改变this指向
  • 作为对象方法调用 -> 一般指向调用对象
  • 在 class 的方法中调用 -> 一般指向实例
  • 箭头函数 -> 指向箭头函数定义作用域

创建 10 个<a>标签,点击的时候弹出来对应的序号

js
// 创建 10 个`<a>`标签,点击的时候弹出来对应的序号
let i, a
for (i = 0; i < 10; i++) {
    a = document.createElement('a')
    a.innerHTML = i + '<br>'
    a.addEventListener('click', function (e) {
        e.preventDefault()
        alert(i)
    })
    document.body.appendChild(a)
}

经典作用域问题,解决方案:

  1. 使用let定义i 形成局部作用域 限制自由变量的查找层级
  2. 通过闭包

实际开发中闭包的应用

参考之前的示例。再次强调闭包中自由变量的寻找方式!!!

js
// 隐藏数据,只提供 API
function createCache() {
    let data = {}  // 闭包中的数据,被隐藏,不被外界访问
    return {
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}
let c = createCache()
c.set('a', 100)
console.log( c.get('a') )

手写 bind 函数

先回顾 bind 函数的应用

js
function fn1(a, b) {
    console.log('this', this)
    console.log(a, b)
}
const fn2 = fn1.bind({ x: 100 }, 10, 20) // bind 返回一个函数,并会绑定上 this
fn2()

手写 bind

js
Function.prototype.bind1 = function () {
    // 将参数解析为数组
    const args = Array.prototype.slice.call(arguments)

    // 获取 this(取出数组第一项,数组剩余的就是传递的参数)
    const t = args.shift()

    const self = this // 当前函数

    // 返回一个函数
    return function () {
        // 执行原函数,并返回结果
        return self.apply(t, args)
    }
}

4. 异步

异步本章节就不再赘述了,上篇异步进阶已经讲解。