跳到主要内容

js 基本数据类型

最新的 ECMAScript 标准定义了 8 种数据类型

  • 7 种原始类型
    • Boolean
    • Number
    • String
    • BigInt
    • Null
    • Undefined
    • Symbol
  • Object

事件循环

async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}

async function async2() {
console.log("async2");
}

console.log("script start");

setTimeout(function () {
console.log("setTimeout");
}, 0);

async1();

new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});

console.log("script end");
  1. 输出 script start

  2. 输出 async1 start

    调用了 async1 函数

  3. 输出 async 2

    调用了 async2 函数

  4. 输出 promise1

    为何不是输出 async1 end?

    因为 async2 函数是异步调用(async await),因此 async2 后面的任务是属于微任务,此时检查主线程,发现还有主线程还有宏任务未调用完(new Promise 内的任务属于宏任务),因此输出 promise 1

  5. 输出 script end

    上一步 Promise 内 resove 之后,直接跳出 Promise 执行 console

  6. 输出 async1 end

    检查发现当前宏任务队列中已经全部执行完毕,开始执行微任务,自上往下按顺序执行

  7. 输出 promise 2

    执行到第二个微任务

  8. 输出 setTimeout

    微任务队列执行完毕,开始执行第二个宏任务

几种判断数据类型的优缺点

typeof

console.log(typeof 1); // number
console.log(typeof true); // boolean
console.log(typeof "mc"); // string
console.log(typeof function () {}); // function
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
console.log(typeof undefined); // undefined
console.log(typeof Symbol()); // symbol
console.log(typeof 10n); // bigint

优点:能够快速区分基本数据类型

缺点:不能判断 object,array 和 null,都返回 object

instanceof

console.log(1 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log(10n instanceof BigInt); // false
console.log(Symbol() instanceof Symbol); // false
console.log("str" instanceof String); // false
console.log([] instanceof Array); // true
console.log(function () {} instanceof Function); // true
console.log({} instanceof Object); // true

优点:能够区分 array,object 和 function,适用于判断自定义的类实例对象

缺点:基本类型不能判断

Object.prototype.toString.call()

const { toString } = Object.prototype;

console.log(toString.call(1)); // [object Number]
console.log(toString.call("str")); // [object String]
console.log(toString.call(true)); // [object Boolean]
console.log(toString.call([])); // [object Array]
console.log(toString.call({})); // [object Object]
console.log(toString.call(function () {})); // [object Function]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]
console.log(toString.call(Symbol())); // [object Symbol]
console.log(toString.call(10n)); // [object BigInt]

优点:精确判断数据类型

缺点:写法繁琐,需要封装

null 和 undefined 的区别

undefined 是访问一个未初始化的变量时返回的值,而 null 是访问一个尚未存在的对象时所返回的值

undefined 看作是空的变量,null 看作是空的对象

实现链式调用

如 (5).add(3).minus(2)

思路:扩展 Number 对象

(function () {
function add(val) {
if (typeof val !== "number" || isNaN(val)) throw new Error("请输入数字");
return this + val;
}
function minus(val) {
if (typeof val !== "number" || isNaN(val)) throw new Error("请输入数字");
return this - val;
}

Number.prototype.add = add;
Number.prototype.minus = minus;
})();

console.log((5).add(3).minus(2)); // 6

创建对象的方式

// 对象字面量
const obj = {};

// 构造函数
function Obj() {}
const obj = new Obj();

// Object.create,此时属性挂载在原型上
const obj = Object.create({ name: "name" });

原型链示意图

原型链示意图

instanceof 原理

用来测试一个对象是否在其原型链中是否存在一个构造函数的 prototype 属性

function Person() {}
function Foo() {}

// 原型继承
Foo.prototype = new Person();
const foo = new Foo();

console.log(foo.__proto__ === Foo.prototype); // true
console.log(foo instanceof Foo); // true
console.log(Foo.prototype.__proto__ === Person.prototype); // true
console.log(foo instanceof Person); // true
console.log(foo instanceof Object); // true

// 更改 Foo.prototype 指向
Foo.prototype = {};
console.log(foo.__proto__ === Foo.prototype); // false
console.log(foo instanceof Foo); // false
console.log(foo instanceof Person); // true
// 手写 instanceof
function _instanceof(leftObj: object, rightObj: object): boolean {
let rightProto = right.prototype; // 右值取原型
leftObj = leftObj.__proto__; // 左值取 __proto__
while (true) {
if (leftObj === null) return false;
else if (leftObj === rightProto) return true;
leftObj = leftObj.__proto__;
}
}

new 运算符原理

  1. 创建一个空对象
  2. 让空对象的 __proto__ (IE 没有该属性) 成员指向构造函数的 prototype 成员对象
  3. 使用 apply 调用构造函数,属性和方法被添加到 this 引用的对象中
  4. 如果构造函数中没有返回其他对象,那么返回 this,即创建的这个新对象;否则,返回构造函数返回的对象
function _new(fn: () => void) {
const obj = Object.create(fn.prototype);
const result = fn.apply(obj);
if (result && (typeof result === "object" || typeof result === "function")) {
// 如果构造函数执行的结果返回的是一个对象,那么返回这个对象
return result;
}
// 如果构造函数返回的不是一个对象,那么返回创建的新对象
return obj;
}

JS 继承

function Father(name) {
// 实例属性
this.name = name;
// 实例方法
this.sayName = function () {
console.log(this.name);
};
}

// 原型属性
Father.prototype.age = 19;
// 原型方法
Father.prototype.sayAge = function () {
console.log(this.age);
};
  1. 原型链继承

    将父类的实例作为子类的原型

    function Son(name) {
    this.name = name;
    }

    Son.prototype = new Father();

    const son = new Son("son");
    son.sayName(); // son
    son.sayAge(); // 19

    优点:

    1. 简单,易于实现
    2. 父类新增原型方法、原型属性,子类都能访问到

    缺点:

    1. 无法实现多继承,因为原型一次只能被一个实例更改
    2. 来自原型对象的所有属性被所有实例共享
    3. 创建子类实例时,无法向父构造函数传参
  2. 构造继承

    复制父类的实例属性给子类

    function Son(name) {
    Father.call(this, "Son props");
    this.name = name;
    }

    const son = new Son("son");
    son.sayName(); // son
    son.sayAge(); // 报错,无法继承父类原型
    console.log(son instanceof Son); // true
    console.log(son instanceof Father); // false

    优点:

    1. 解决了原型链继承中实例共享父类引用属性的问题
    2. 创建子类实例时,可以向父类传参
    3. 可以实现多继承(call 多个父类对象)

    缺点:

    1. 实例并不是父类的实例,只是子类的实例
    2. 只能继承父类实例属性和方法,不能继承原型属性和方法
    3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
  3. 组合继承

    将原型链和构造函数组合一起,使用原型链实现对原型属性和方法的继承,使用构造函数实现实例属性继承

    function Son(name) {
    // 第一次调用父类构造器 子类实例增加父类实例
    Father.call(this, "Son props");
    this.name = name;
    }

    Son.prototype = new Father();

    const son = new Son("son");
    son.name; // 'son'
    son.sayAge(); // 19
    son.sayName(); // 'son'
    son instanceof Son; // true
    son instanceof Father; // true
    son.constructor === Father; // true
    son.constructor === Son; // false

    优点:

    1. 弥补了构造继承的缺点,可以同时继承实例属性方法和原型属性方法
    2. 既是子类的实例,也是父类的实例
    3. 可以向父类传参
    4. 函数可以复用

    缺点:

    1. 调用了两次父类构造函数,生成了两份实例
    2. 构造函数(constructor)指向问题
  4. 实例继承

    为父类实例添加新特性,作为子类实例返回

    function Son(name) {
    const father = new Father("Son Props");
    father.name = name;
    return father;
    }

    const son = new Son("son");
    son.name; // son
    son.sayAge(); // 19
    son.sayName; // son
    son instanceof Father; // true
    son instanceof Son; // false
    son.constructor === Father; // true
    son.constructor === Son; // false

    优点:

    1. 不限制调用方式

    缺点:

    1. 实例是父类的实例,不是子类的实例
    2. 不支持多继承
  5. 拷贝继承

    对父类实例中的方法和属性拷贝给子类的原型

    function Son(name) {
    const father = new Father("Son props");
    for (let i in father) {
    Son.prototype[i] = father[i];
    }
    Son.prototype.name = name;
    }

    const son = new Son("son");
    son.sayAge(); // 19
    son.sayName(); // son
    son instanceof Father; // false
    son instanceof Son; // true
    son.constructor === Father; // false
    son.constructor === Son; // true

    优点:

    1. 支持多继承

    缺点:

    1. 效率低,性能差,内存占用高(因为需要拷贝父类属性)
    2. 无法获取父类不可枚举的方法
  6. 寄生组合继承

    通过寄生方式,砍掉父类的实例属性,避免组合继承生成两份实例的缺点

    function Son(name) {
    Father.call(this);
    this.name = name;
    }

    Son.prototype = Object.create(Father.prototype);
    Son.prototype.constructor = Son;

    const son = new Son("son");
    son.sayAge; // 19
    son.sayName; // son
    son instanceof Father; // true
    son instanceof Son; // true
    son.constructor === Father; // false
    son.constructor === Son; // true

    优点:

    1. 比较完美(js 实现继承首选方式)

    缺点:

    1. 实现方式较为复杂

防抖与节流

防抖

动作停止后的时间超过设定的时间时执行一次函数。

注意:这里的动作停止表示你停止触发函数,从这个时间点开始计算,当间隔时间等于你设定的时间,才会执行里面的回调函数

function debounce(fn: () => void, delay: number) {
let timer: number | null = null;
return function () {
if (timer) window.clearTimeout(timer);
timer = window.setTimeout(function () {
fn();
}, delay);
};
}

window.addEventListener(
"scroll",
debounce(() => {
console.log("scroll");
}, 1000)
);

节流

一定时间内执行的操作只会执行一次,也就是预先设定一个执行周期,当调用动作的即刻大于等于执行周期时执行该动作,然后进入下一个周期

function throttle(fn: () => void, delay: number) {
let flag = true;
return function () {
if (!flag) return;
flag = false;
setTimeout(function () {
fn();
flag = true;
}, delay);
};
}

运行机制

单线程

作为浏览器的脚本语言,因此 JS 注定是单线程

单线程同一时间只能做一件事,为了解决这个问题,JS 的设计者将所有任务分为两种:同步任务(synchronous)和异步任务(asynchronous)

同步任务和异步任务

同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务

异步任务:不进入主线程,而是进入任务队列(task queue),只有任务队列通知主线程,该任务才会进入主线程执行

任务队列

任务队列

异步任务会先到事件列表注册函数,如果事件列表中的事件触发了,会将这个函数移入任务队列中(DOM 操作对应 DOM 事件,资源加载操作对应加载事件,定时器操作可以看作一个‘时间到了’的事件)

宏任务和微任务

宏任务和微任务

  • 宏任务

    整体代码 script,setTimeout,setInterval,setImmediate(仅在 Node 环境),I/O,requestAnimationFrame(仅在浏览器环境)

  • 微任务

    Promise.then | catch | finallyprocess.nextTick(仅在 Node 环境),MutationObserver(仅在浏览器环境)

事件循环(Event Loop)

事件循环示意图

  1. 整体 script 作为第一个宏任务开始执行,此时会把所有代码分为“同步任务”和“异步任务”两部分
  2. 同步任务直接进入主线程依次执行
  3. 异步任务再分为宏任务微任务
  4. 宏任务进入事件列表中,并在里面注册回调函数,每当指定的事件完成时,事件列表会将这个函数移到任务队列中
  5. 微任务也会进入另一个事件列表,并执行第 4 步一样的操作
  6. 当主线程的任务执行完毕,主线程为空,此时会检查微任务的任务队列,如果有任务,就全部执行,没有就执行下一个宏任务
  7. 上述过程不断重复,这就是事件循环(Event Loop)