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");
输出 script start
输出 async1 start
调用了 async1 函数
输出 async 2
调用了 async2 函数
输出 promise1
为何不是输出 async1 end?
因为 async2 函数是异步调用(async await),因此 async2 后面的任务是属于微任务,此时检查主线程,发现还有主线程还有宏任务未调用完(new Promise 内的任务属于宏任务),因此输出 promise 1
输出 script end
上一步 Promise 内 resove 之后,直接跳出 Promise 执行 console
输出 async1 end
检查发现当前宏任务队列中已经全部执行完毕,开始执行微任务,自上往下按顺序执行
输出 promise 2
执行到第二个微任务
输出 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 运算符原理
- 创建一个空对象
- 让空对象的
__proto__
(IE 没有该属性) 成员指向构造函数的 prototype 成员对象 - 使用 apply 调用构造函数,属性和方法被添加到 this 引用的对象中
- 如果构造函数中没有返回其他对象,那么返回 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);
};
原型链继承
将父类的实例作为子类的原型
function Son(name) {
this.name = name;
}
Son.prototype = new Father();
const son = new Son("son");
son.sayName(); // son
son.sayAge(); // 19优点:
- 简单,易于实现
- 父类新增原型方法、原型属性,子类都能访问到
缺点:
- 无法实现多继承,因为原型一次只能被一个实例更改
- 来自原型对象的所有属性被所有实例共享
- 创建子类实例时,无法向父构造函数传参
构造继承
复制父类的实例属性给子类
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优点:
- 解决了原型链继承中实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传参
- 可以实现多继承(call 多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类实例属性和方法,不能继承原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
组合继承
将原型链和构造函数组合一起,使用原型链实现对原型属性和方法的继承,使用构造函数实现实例属性继承
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优点:
- 弥补了构造继承的缺点,可以同时继承实例属性方法和原型属性方法
- 既是子类的实例,也是父类的实例
- 可以向父类传参
- 函数可以复用
缺点:
- 调用了两次父类构造函数,生成了两份实例
- 构造函数(constructor)指向问题
实例继承
为父类实例添加新特性,作为子类实例返回
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优点:
- 不限制调用方式
缺点:
- 实例是父类的实例,不是子类的实例
- 不支持多继承
拷贝继承
对父类实例中的方法和属性拷贝给子类的原型
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优点:
- 支持多继承
缺点:
- 效率低,性能差,内存占用高(因为需要拷贝父类属性)
- 无法获取父类不可枚举的方法
寄生组合继承
通过寄生方式,砍掉父类的实例属性,避免组合继承生成两份实例的缺点
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优点:
- 比较完美(js 实现继承首选方式)
缺点:
- 实现方式较为复杂
防抖与节流
防抖
动作停止后的时间超过设定的时间时执行一次函数。
注意:这里的动作停止表示你停止触发函数,从这个时间点开始计算,当间隔时间等于你设定的时间,才会执行里面的回调函数
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 | finally
,process.nextTick
(仅在 Node 环境),MutationObserver(仅在浏览器环境)
事件循环(Event Loop)
- 整体 script 作为第一个宏任务开始执行,此时会把所有代码分为“同步任务”和“异步任务”两部分
- 同步任务直接进入主线程依次执行
- 异步任务再分为宏任务和微任务
- 宏任务进入事件列表中,并在里面注册回调函数,每当指定的事件完成时,事件列表会将这个函数移到任务队列中
- 微任务也会进入另一个事件列表,并执行第 4 步一样的操作
- 当主线程的任务执行完毕,主线程为空,此时会检查微任务的任务队列,如果有任务,就全部执行,没有就执行下一个宏任务
- 上述过程不断重复,这就是事件循环(Event Loop)