蒙奇·D·路飞,日本漫画《航海王》及其衍生作品中的男主角,绰号“草帽”路飞。而 JavaScript 则是前端的主角,篇记录自己整理的 JavaScript 基础内容,不定期更新,如有不对的地方,还请大家指出,会及时改正,多多包涵。
js 数据类型
- 基本类型:Number、String、Boolean、Undefined、Null、Symbol、BigInt
- 引用类型:Object (Array、Function、Object、Date、RegExp、Map、Set、WeakMap、WeakSet)
js 类型判断
typeof
在 JavaScript 最初的实现中,typeof 的返回值是基于表示值的机器码类型来决定的
- 000:表示对象,数据是对象的引用。
- 1:表示整型,数据是 31 位带符号整数。
- 010:表示双精度类型,数据是双精度数字。
- 100:表示字符串,数据是字符串。
- 110:表示布尔类型,数据是布尔值。
null 的机器码刚好都是 0,所以 typeof null 返回 object。但是在发现这个问题的时候 js 已经普及了,所以就保留了这个问题。
// typeof 用于判断基本类型,null 由于
typeof 1; // number
typeof "1"; // string
typeof true; // boolean
typeof undefined; // undefined
typeof Symbol(); // symbol
typeof null; // object 特殊情况
typeof function () {}; // function 为了突出 function 的特殊
// 其他返回都是 object
instanceof
instanceof 是用来判断一个对象是否是某个类的实例,它返回一个布尔值。
const a = [];
a instanceof Array; // true
a instanceof Object; // true
a instanceof Function; // false
const b = new Number(1);
b instanceof Number; // true
b instanceof Object; // true
// 手写 instanceof
function myInstanceof(left, right) {
// 如果不是对象直接返回 false
if (typeof left !== "object" || left === null) return false;
let proto = left.__proto__;
let prototype = right.prototype;
while (proto) {
if (proto === prototype) return true;
proto = proto.__proto__;
return false;
}
}
自己定义的对象可以串改原型链导致判断错误,内置的 Object、Array 等不会
console.log(Object.getOwnPropertyDescriptor(Object, "prototype"));
// {value: {…}, writable: false, enumerable: false, configurable: false}
如果项目中存在多个环境,如:frames 也会有问题,参考 MDN
Object.prototype.toString.call
Object.prototype.toString 可以准确的判断出数据类型。但也有需要注意的点
- 为什么需要 call ?
每个对象都有一个 toString 方法,默认情况下返回"[object type]",其中 type 是对象的类型。 像 Array、Date、Function 等都是 Object 的实例,但是它们都重写了 toString 方法。当我们直接调用它们的 toString 方法时,会返回一个字符串,而不是"[object type]"的格式
Array.prototype.toString([]); // ''
String.prototype.toString("a"); // ''
由于不同类型的对象都重写了 toString 方法,所以我们需要使用 Object.prototype.toString 来判断数据类型。但是直接调用 Object.prototype.toString 时,this 指向的是 Object.prototype,返回都是"[object object]",而不是我们想要判断的对象。所以需要使用 call 方法来改变 toString 方法的执行上下文,让它指向我们传入的对象。
当然,如果硬要搞事情,
Object.prototype.toString.call在自己创建的类中可能会有问题,不包括内置对象同理 instanceof
class MyClass {
get [Symbol.toStringTag]() {
return "路飞";
}
}
Object.prototype.toString.call(new MyClass()); // "[object 路飞]"
闭包
有权访问另一个函数作用域中的变量的函数
function foo() {
let a = 1;
return function bar() {
console.log(a);
};
}
let fn = foo();
fn();
作用域
作用域,变量和函数能被访问的区域
全局作用域
let a = 1;
function foo() {
console.log(a);
}
foo(); // a 作用域链: 在foo函数中没有找到 a 变量,向上查找,在全局中找到了 a 变量,打印出1
函数作用域
function foo() {
let a = 1;
console.log(a);
}
foo(); // 1
console.log(a); // ReferenceError: a is not defined
块级作用域
{
var a = 1;
let b = 2; // es6 新增,let 和 const 都是块级作用域。
}
console.log(a); // 1
console.log(b); // ReferenceError: a is not defined
原型、原型链
__proto__已被弃用,建议使用Object.getPrototypeOf代替。
// 为了看起来更清晰,这里继续使用 __proto__
function Person() {}
var person1 = new Person();
// 构造器
console.log(Person.prototype.constructor === Person); // 原型的构造器指向构造函数
console.log(Person.__proto__ === Function.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);
// 实例
console.log(person1.__proto__ === Person.prototype);
console.log(Person.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);
继承
原型链继承
function Parent() {
this.name = "parent";
this.age = 18;
this.arr = [1, 2];
}
Parent.prototype.say = function () {
console.log("hello");
};
function Child() {
this.name = "child";
}
Child.prototype = new Parent();
// Child.prototype.constructor = Child; // 修改 constructor
let child1 = new Child();
let child2 = new Child();
console.log(child1.name); // child
console.log(child1.age); // 18 继承自父类
child1.say(); // hello
console.log(child1.arr); // [1,2]
// 修改child1的arr会影响child2,因为他们都是引用的同一个对象
child1.arr.push(3);
console.log(child2.arr); // [1,2,3]
- 如果改变实例的引用类型属性,会影响其他实例中的属性
- 构造函数被改变
构造函数继承
function Parent(name) {
this.name = name;
this.age = 18;
this.arr = [1, 2];
}
Parent.prototype.say = function () {
console.log("hello");
};
function Child(name) {
Parent.call(this, name);
}
let child1 = new Child("路飞");
let child2 = new Child("索隆");
child1.arr.push(3);
console.log(child1.arr); // [1, 2, 3]
console.log(child2.arr); // [1, 2]
child1.say(); // child1.say is not a function
- 不能访问父类原型上的属性
组合继承
function Parent(name) {
this.name = name;
this.age = 18;
this.arr = [1, 2];
console.log("parent");
}
Parent.prototype.say = function () {
console.log("hello");
};
function Child(name) {
Parent.call(this, name);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let child1 = new Child("路飞");
// parent
// parent
- 解决上述两种继承的缺陷,但是会执行两次父类构造函数。
原型式继承
let parent = {
name: "a",
arr: [1, 2],
};
let child1 = Object.create(parent);
let child2 = Object.create(parent);
child1.arr.push(3);
console.log(child2.arr); // [1,2,3]
- 如果改变实例的引用类型属性,会影响其他实例中的属性
寄生式继承
let parent = {
name: "parent",
arr: [1, 2],
};
function childFn(obj) {
let childObj = Object.create(obj);
childObj.name = "child";
return childObj;
}
let child = childFn(parent);
- 和原型式继承一样,如果改变实例的引用类型属性,会影响其他实例中的属性,只是可以自己添加额外属性
寄生组合式继承
function Parent(name) {
this.name = name;
this.age = 18;
this.arr = [1, 2];
}
Parent.prototype.say = function () {
console.log("hello");
};
function Child(name) {
Parent.call(this, name);
}
//Child.prototype = new Parent(); 解决了组合继承执行父类构造函数两次的问题
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
let child1 = new Child("路飞");
- 避免父类构造函数多次调用
- 子类实例独立的引用空间,避免因为修改引用类型对其他实例的影响
- 可以访问父类原型属性
class 继承
class Parent {
constructor(name) {
this.name = name;
this.age = 50;
}
say() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name) {
super(name);
this.age = 18;
}
}
let child1 = new Child("路飞");
let child2 = new Child("索隆");
new 创建实例的过程、手写一个 new
- 创建一个新的空对象
- 将空对象的
[[Prototype]]指向构造函数的 prototype - 调用构造函数,并将 this 指向新对象
- 如果构造函数返回的是引用类型,则返回这个引用类型,否则返回新对象
function myNew(fn, ...args) {
let obj = Object.create(fn.prototype);
let res = fn.apply(obj, args);
return typeof res === "object" ? res : obj;
}
new.target 可以用来判断是否是 new 调用,如果是 new 调用,则返回构造函数或者函数的引用,普通函数返回 undefined
this
globalThis 获取不同 javascript 环境下的全局对象
this 的取值取决于函数如何被调用
// this 永远指向最后调用它的对象
// 例子1
let obj = {
a: 1,
fn: function () {
console.log(this);
},
};
obj.fn(); // {a: 1, fn: ƒ}
let fn = obj.fn;
fn(); // Window
// 例子2
function fn() {
console.log(this);
}
let obj = {
a: 1,
};
obj.fn = fn;
person(); // Window
obj.fn(); // {a: 1, fn: ƒ}
new 绑定
通过上面 new 创建实例的过程,可以知道,当函数通过 new 调用时,this 会绑定到新创建的对象上,所以 this 在 new 调用中指向新创建的对象。如果在执行构造函数时返回了引用类型,那么指向这个引用类型。
function Parent() {
this.name = "parent";
}
let child = new Parent();
console.log(child.name); // parent
function Person() {
this.name = "Person";
return {};
}
let child = new Parent();
console.log(child.name); // undefined
箭头函数
箭头函数并没有自己的 this 值,它捕获定义时所在上下文的 this 值作为自己的 this 值。
// 箭头函数 fn 的执行上下文是全局 window,怎么调用都是指向 window
var name = "a";
var fn = () => {
console.log(this);
};
fn(); //window
var obj = { name: "b", fn: fn };
obj.fn(); //window
fn.call({ name: "c" }); //window
// 普通函数 fn 的执行上下文是全局对象,所以箭头函数 test 的执行上下文也是全局对象,所以 this 指向 window
function fn() {
this.a = 1;
let test = () => {
console.log(this, this.a); // window 1
};
test();
}
fn();
// 普通函数 fn 的执行上下文是 obj,所以箭头函数 test 的执行上下文也是 obj, 在执行中 fn2 函数改变了 a 的值,所以打印 a 为 1
function fn2() {
this.a = 1;
let test = () => {
console.log(this); // {a: 1, b: 'bb', fn: ƒ}
};
test();
}
const obj = {
a: 2,
b: "bb",
};
obj.fn = fn2;
obj.fn();
手写 call、apply、bind
call、apply、bind 可以改变 this 的指向
call
Function.prototype.myCall = function (context = globalThis, ...args) {
// if (typeof this !== "function") {
// //不需要判断类型,因为myCall定义在Function.prototype上
// throw new TypeError(`${this} is not a function!`);
// }
// 1. 确定要绑定的对象,即最终谁来调用函数,命名为new_this;若没有传入要绑定的对象, 默认绑定window对象
// null 和 undefined 将被替换为全局对象,并且原始值将被转换为对象。例:typeof context 为 number, new_this = new Number(context),本例没有考虑
const new_this = context;
// 2. 把方法作为对象的属性绑定给new_this,但要注意,也许原有属性就有func,为了避免冲突,这里用symbol
const func = Symbol("func");
new_this[func] = this;
// 3. 执行当前函数,并获取返回值
const res = new_this[func](...args);
// 4. 删除我们绑定的的Symbol(func)属性,以免污染new_this的属性
delete new_this[func];
// 5. 返回第3步得到的返回值
return res;
};
apply
// 和 call 类似,参数是数组
Function.prototype.myApply = function (context = globalThis, args) {
if (!Array.isArray(args)) {
throw new TypeError(`args is not an array!`);
}
const new_this = context;
const func = Symbol("func");
new_this[func] = this;
const res = new_this[func](...args);
delete new_this[func];
return res;
};
bind
Function.prototype.myBind = function (context = globalThis, ...args) {
// 把原函数(即this)用一个fn变量保存一下
let fn = this;
return function newFn(...fnArgs) {
// 要考虑新函数是不是会当作构造函数
if (this instanceof newFn) {
// 如果是构造函数则调用new 并且合并参数args,fnArgs
return new fn(...args, ...fnArgs);
}
// 当作普通函数调用 也可以用上面定义的myCall
return fn.call(context, ...args, ...fnArgs);
};
};
防抖、节流
防抖
搜索输入框,在连续输入的过程中不会触发请求,等输入完成等待 delay 时间后触发请求。
function debounce(fn, delay) {
let timer = null;
return function () {
let context = this;
let args = arguments;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
};
}
节流
搜索框,在连续输入的过程中,每隔 delay 时间就触发一次请求。
function throttle(fn, delay) {
let timer = null;
return function () {
let context = this;
let args = arguments;
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
};
}
function throttle(fn, delay) {
let time = Date.now();
return function () {
let context = this;
let args = arguments;
let newTime = Date.now();
if (newTime - time >= delay) {
fn.apply(context, args);
time = newTime;
}
};
}
var、let、const
变量提升、暂时性死区
var 可以在申明之前访问,let、const 则不行
console.log(a); // undefined
var a = 1;
console.log(b); // Cannot access 'b' before initialization
let b = 2;
console.log(c); // Cannot access 'c' before initialization
const c = 3;
块级作用域
var 没有块级作用域,let、const 有
{
var a = 1;
}
console.log(a); // 1
{
let b = 2;
}
console.log(b); // b is not defined
{
const c = 3;
}
console.log(c); // c is not defined
重复声明
var a = 1;
var a = 2;
//IDE 里面就会报错
let b = 2;
let b = 3; // Identifier 'b' has already been declared
const c = 3;
const c = 4; // Identifier 'c' has already been declared
重新赋值
const 用来定义常量,不能重新赋值
const c = 3;
c = 4; // Assignment to constant variable.
Promise
异步编程的解决方案,比传统回调函数更方便清晰,处理回调地狱。
链式调用操作,代码可读性强。
状态
三种状态:pending、fulfilled、rejected,状态一旦改变无法再变化。
流程
MDN 的流程图
静态方法
Promise.all
该方法返回一个 Promise,只有当所有 Promise 都成功时,才会成功,只要有一个失败,则返回失败。
Proimse.allSettled
该方法返回一个 Promise,只有当所有 Promise 不管成功或者失败都完成时,才会完成。
Promise.any
该方法返回一个 Promise,返回任意第一个成功的 Promise。如果全部失败,返回信息:All promises were rejected
Promise.race
该方法返回一个 Promise,返回第一个完成的 Promise。
Promise.resolve
直接返回一个 fulfilled 状态的 Promise
Promise.reject
直接返回一个 rejected 状态的 Promise
Promose.withResolvers
const fn1 = () => {
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => {
resolve(1);
}, 1000);
return promise;
};
fn1().then((value) => console.log(value));
// 等价于
const fn2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("2");
}, 1000);
});
};
fn2().then((res) => console.log(res));
实例方法
then
Promise 链式调用的关键方法,最多接受 2 个参数,第一个参数是成功回调,第二个参数是失败回调。
catch
Promise 失败的回调,相当于 then(undefined, onRejected)
finally
在 Promise 不管是成功还是失败之后调用,finally 并不会结束后面的调用。
const fn = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("2");
}, 1000);
});
};
fn()
.then((res) => console.log(res))
.catch((e) => console.log(e))
.finally(() => console.log("finally"))
.then(() => console.log("last then"));
// 2
// finally
// bbb
0.1 + 0.2 !== 0.3
js 数字以浮点数的形式存储,0.1、0.2 在转换成二进制时,无限循环导致无法精确表示
原理
小数转换成二进制的方法,将小数部分乘以 2,取整数部分,剩余小数继续乘,直到小数部分为 0,以 0.2 为例
-
0.2 * 2 = 0.4,整数部分 0,小数部分 4 --0.0
-
0.4 * 2 = 0.8,整数部分 0,小数部分 8 --0.00
-
0.8 * 2 = 1.6,整数部分 1,小数部分 6 --0.001
-
0.6 * 2 = 1.2,整数部分 1,小数部分 2 --0.0011
-
0.2 * 2 = 0.4,整数部分 0,小数部分 4 --0.00110 开始循环
0.2: 0.0011 0011 0011 0011 ...
0.1: 0.0001 1001 1001 1001 ...
// 验证
(0.2).toString(2)(0.1).toString(2);
两数和:0.0100 1100 1100 1100 ...
IEEE 754 标准,64 位表示双精度浮点数,1 位符号位,11 位指数偏移量,剩下 52 位表示小数部分
按照(IEEE 754 标准)保留 52 位,然后转换成十进制就变成了:0.30000000000000004
解决方案
- toFixed
(0.1 + 0.2).toFixed(1); // 0.3
// 存在问题,有时候不准确
(1.335).toFixed(2); // 1.33
- 转成整数
(0.1 * 10 + 0.2 * 10) / 10; // 0.3
// 不保险
10.2 * 100; // 1019.9999999999999
(10.2 * 100 + 0.02 * 100) / 100; // 10.219999999999999
- 第三方库
for...in、for...of
for...in
遍历对象所有可枚举字符串(除 Symbol)
const sym = Symbol("a");
const object = { a: 1, b: 2, c: 3 };
object[sym] = 4;
for (const i in object) {
console.log(i); // a b c
}
const arr = [1, 2, 3];
for (const i in arr) {
console.log(i); // 0 1 2
}
for...of
循环一个可迭代对象的值
const arr = [1, 2, 3];
for (const i of arr) {
console.log(i); // 1 2 3
}
// 对象不可迭代
const object = { a: 1, b: 2, c: 3 };
for (const i of object) {
console.log(i); // object is not iterable
}
如果要实现对象的迭代,可以自己实现一个迭代方法,需要实现 Symbol.iterator 方法。
const a = [];
console.log(a[Symbol.iterator]); // ƒ values() { [native code] }
const b = {};
console.log(b[Symbol.iterator]); // undefined
const arr = [1, 2, 3];
for (const i of arr) {
console.log(i); // 1 2 3
}
// 等价于下面内容
const iterator = arr[Symbol.iterator]();
for (const i of iterator) {
console.log(i); // 1 2 3
}
const obj = { a: 11, b: 22, c: 33 };
// 所以便利对象可以自己实现一个 Symbol.iterator 方法
obj[Symbol.iterator] = function () {
return Object.values(this)[Symbol.iterator]();
};
for (const i of obj) {
console.log(i); // 11 22 33
}
遍历对象的几种方法
let obj = { name: "a" };
const sym = Symbol("objSymbol");
obj[sym] = "b";
Object.defineProperty(obj, "age", {
value: 18,
});
console.log(obj);
// 遍历可枚举属性(除 symbol)
for (const i in obj) {
console.log(i); // name
}
//遍历可枚举属性键值对(除 symbol)
for (const i of Object.entries(obj)) {
console.log(i); // ['name', 'a']
}
// 遍历所有属性(除 symbol)
for (const i of Object.getOwnPropertyNames(obj)) {
console.log(i); // name age
}
// 遍历 symbol 属性
for (const i of Object.getOwnPropertySymbols(obj)) {
console.log(i); // Symbol(objSymbol)
}
// 遍历所有属性
for (const i of Reflect.ownKeys(obj)) {
console.log(i); // name age Symbol(objSymbol)(objSymbol)
}
// 遍历所有属性的描述信息
console.log(Object.getOwnPropertyDescriptors(obj)); // 返回所有对象属性 name age Symbol(objSymbol)
// {name: {value: 'a', writable: true, enumerable: true, configurable: true}...}