前端 OnePiece JavaScript (路飞)篇

81 阅读12分钟

蒙奇·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();

image.png

作用域

作用域,变量和函数能被访问的区域

全局作用域

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);

image-2.png

image-4.png

继承

原型链继承

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

  1. 创建一个新的空对象
  2. 将空对象的 [[Prototype]] 指向构造函数的 prototype
  3. 调用构造函数,并将 this 指向新对象
  4. 如果构造函数返回的是引用类型,则返回这个引用类型,否则返回新对象
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 的流程图

image.png

静态方法

  • 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
  • 第三方库

big.jsdecimal.js

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}...}