JavaScript 面试必看

this 指向

  1. 全局环境中:无论是严格还是非严格模式,this 都指向 window。
"use strict"; // 开启严格模式
console.log(this); // window
console.log(this); // window
  1. 函数内部:

    • 在非严格模式下,直接调用 this,this 指向 window。
    function fn() {
      console.log(this); // window
    }
    fn();
    
    • 在严格模式下,直接调用 this,this 指向 undefined。
    "use strict";
    function fn() {
      console.log(this); // undefined
    }
    fn();
    
    • 对象方法中调用,谁调用 this 就是谁。
    const user = {
      name: "curry",
      play() {
        console.log(this); // user
      },
    };
    user.play();
    

改变 this 指向

注意:

  1. call 和 apply 的作用相同,但是传参方式不同。
  2. call() 和 apply() 会立即执行函数。
  3. bind() 不会立即调用函数,而是返回一个已绑定 this 值的新函数,需要手动调用这个新函数来执行操作。
call()
// fn.call(需要指向的对象,参数 1,参数 2,...)
function fn(a, b) {
  console.log(this); // user
  console.log(a, b); // 1 2
}
const user = {
  name: "curry",
};
fn.call(user, 1, 2);
apply()
// fn.apply(需要指向的对象,[参数 1,参数 2,...])
function fn(a, b) {
  console.log(this); // user
  console.log(a, b); // 11 22
}
const user = {
  name: "curry",
};
fn.apply(user, [11, 22]);
bind()
// fn.bind(需要指向的对象,参数 1,参数 2,...)
function fn(a, b) {
  console.log(this); // user
  console.log(a, b); // 666 999
}
const user = {
  name: "curry",
};
const handleClick = fn.bind(user, 666);
handleClick(999);
箭头函数
const user = {
  name: "curry",
  play() {
    console.log(this); // user
    setTimeout(() => {
      console.log(this); // user
    }, 1000);
    setTimeout(function () {
      console.log(this); // window
    }, 2000);
  },
};
user.play();

手写 bind

// 1. 定义myBind函数
Function.prototype.myBind = function (thisArg, ...args) {
  console.log(...args);
  // 2. 返回绑定this的新函数
  return (...reArgs) => {
    // 3. 合并参数(注意先后顺序)
    // args 是绑定的参数
    // reArgs 是调用的新函数的参数
    return this.call(thisArg, ...args, ...reArgs); // 这里将this修改为传递进来的对象
  };
};

const user = { name: "Joe" };

function fn(num1, num2, num3, num4) {
  console.log(this);
  console.log(num1, num2, num3, num4);
  return num1 + num2 + num3 + num4;
}

const handleClick = fn.myBind(user, 2, 4);
const res = handleClick(6, 8);
console.log(res);

继承

  • 继承可以使子类使用父类的属性和方法,不需要重新编写相同的代码。
Class 实现继承
  • 类(class)是用于创建对象的模板,类建立在原型上
// 基本语法

/**
 * static:用来定义静态方法,注意,静态方法只能通过类(Car)本身调用,不能在类的实例(c1)上调用
 * #:类属性默认情况下是公有的,可以使用增加哈希前缀(#)来定义私有类,声明和访问时也需要加上,注意:只能在类的内部来使用
 */

class Car {
  // 公有属性
  name;
  color = "白色"; // 定义并设置默认值

  // 静态属性
  static type = "私家车";

  // 静态方法
  static reverse() {
    console.log("我会掉头!");
  }

  // 私有属性
  #level = "B级车";

  // 私有方法
  #func() {
    console.log("下雨不会被淋湿。。。");
  }

  // 构造函数
  constructor(name) {
    this.name = name;
  }

  // 公有方法
  run() {
    console.log("i can run ~~~");
    console.log(this.name); // 这里的this指的是Car的实例对象
  }

  // 定义方法,访问私有属性和方法(私有属性和方法只能在Car的内部来访问)
  applyPrivate() {
    console.log(this.#level);
    this.#func();
  }
}

const c1 = new Car("大众");

console.log(Car.type); // 调用静态属性
Car.reverse(); // 调用静态方法

c1.applyPrivate();

// 实现继承
/**
 * extends:用来创建呢一个类,这个类是另外一个类的子类。
 * super:一般用来调用父类的构造函数
 */

// 新建一个叫 Volkswagen 的类,继承自 Car
class Volkswagen extends Car {
  long;

  constructor(name, long) {
    // 通过super关键字,调用父类的构造函数
    super(name);
    this.long = long || "4866mm";
  }
  // 如果方法名称与父类相同,则按照就近原则调用
  run() {
    console.log("跑起来了");
  }
  say() {
    console.log("我是Volkswagen", `我是${this.name}`);
  }
}

const v1 = new Volkswagen("迈腾");
const v2 = new Volkswagen("高尔夫", "4949mm");
原型和构造函数继承(寄生组合式继承)
  • 通过借用构造函数来继承属性,通过原型链来继承方法。
// 父类构造函数
function Car(name, color) {
  this.name = name;
}
Car.prototype.run = function () {
  console.log("i can run");
};

// 子类构造函数
function Volkswagen(name) {
  Car.call(this, name);
}

const c = new Volkswagen("大众");
console.log(c); //  {name: '大众'}

// 基于父类的原型创建一个新的原型对象
/**
 * Object.create():将一个对象作为原型,创建一个新的对象
 * 参数1:要根据哪个对象创建
 * 参数2:要覆盖对象的哪个属性
 */
const prototype = Object.create(Car.prototype, {
  constructor: {
    value: Volkswagen,
  },
});
// 将创建的原型赋值给子类的原型
Volkswagen.prototype = prototype;

const v = new Volkswagen("迈腾");
console.log(v); // {name: '迈腾'}
v.run(); // i can run

fetch

  • 浏览器内置的 api,用于发送网络请求。
  • 返回一个 Promise 对象,可以使用 then() 方法来处理请求结果。
async function postData() {
  const headers = new Headers();
  headers.append("Content-Type", "application/json");
  const res = await fetch("http://********/api/register", {
    method: "POST",
    headers,
    body: JSON.stringify({
      username: "admin",
      password: "123456",
    }),
  });
  if (res.status === 200) {
    const data = await res.json();
    console.log(data);
  } else {
    console.log("请求失败", res.status);
  }
}
postData();

Generator

  • Generator 函数是 ES6 提供的一种异步编程解决方案,语法上,Generator 函数是一个普通函数,但是它用星号(*)标记,并且带有 yield 表达式。
    注:在异步处理中,async / await 是最常用的,Generator 了解下就可以。
// 定义生成器函数
function* userGenerator() {
  console.log("被执行。");
  yield "北京";
  yield "朝阳";
  yield "三元桥";
}

// 获取Generator对象
const g = userGenerator();

// next方法
// 调用next方法可以获取yield的值,{value:yeld的值,done:是否执行完毕}
// g.next()

// for of
for (const iterator of g) {
  console.log(iterator); // yeld的值
}

// 实例
function* cityGenerator() {
  console.log("被执行。");
  yield fetch("http://********/api/city?pname=北京");
  yield fetch("http://********/api/city?pname=广东省");
}
const city = cityGenerator();
city
  .next()
  .value.then((res) => {
    console.log(res);
    return res.json();
  })
  .then((data) => {
    console.log(data);
    return city.next().value;
  })
  .then((res) => {
    return res.json();
  })
  .then((data) => {
    console.log(data);
  });

函数柯里化

  • 什么是柯里化?
    在使用 JavaScript 编写代码的时候,有一种函数式编程的思想,而提到函数式编程,一定绕不开一个概念,那就是柯里化。柯里化是编程语言中的一个通用的概念(不只是 Js,其他很多语言也有柯里化),是指把接收多个参数的函数变换成接收单一参数的函数,嵌套返回直到所有参数都被使用并返回最终结果。更简单地说,柯里化是一个函数变换的过程,是将函数从调用方式:f(a,b,c)变换成调用方式:f(a)(b)©的过程。柯里化不会调用函数,它只是对函数进行转换。
    简单来说,就是将多个参数的函数转换为单个参数的函数。

  • 例子:

function sum(a, b) {
  return a + b;
}
console.log(sum(1, 2)); // 3

// 转换后
function sum(a) {
  return function (b) {
    return a + b;
  };
}
console.log(sum(1)(2)); // 3
  • 函数柯里化:
// 接收一个长度,并返回一个函数,长度是用来解决几个长度累加,返回函数是用来创建一个新的函数,并且调用、传参
function sumMaker(length) {
  // nums 用来保存函数不固定的参数
  let nums = [];
  function partialSum(...args) {
    nums.push(...args);
    // 判断参数的个数
    if (nums.length >= length) {
      // 长度大于5的情况下,就将参数累加并返回
      const res = nums.slice(0, length).reduce((a, b) => a + b, 0);
      // 累加完之后,将nums清空
      nums = [];
      return res;
    } else {
      // 不满足5的情况下,返回partialSum函数
      return partialSum;
    }
  }

  return partialSum;
}

const sum7 = sumMaker(7);
console.log(sum7(1)(2)(3)(4)(5)(6)(7));
  • 实际应用:
const typeOf = (type) => {
  return (thing) => typeof thing === type;
};

const isString = typeOf("string");
console.log(isString("abc")); // true

const isNumber = typeOf("number");
console.log(isNumber(123)); // true
console.log(isNumber("123")); // false

设计模式

  1. 工厂模式
  • 定义:一个工厂函数,根据传入的参数,返回不同的对象。
function createCar(name, color) {
  return {
    name,
    color,
    run() {
      console.log("跑");
    },
  };
}

const c1 = createCar("奔驰", "red");
const c2 = createCar("宝马", "blue");
console.log(c1 === c2); // false
  1. 单例模式
  • 定义:一个类只能有一个实例,即使多次实例化该类,也只返回第一次实例化后的实例对象。
class SingleTon {
  // 私有属性
  static #instance;

  // 添加静态方法
  static getInstance() {
    if (this.#instance === undefined) {
      this.#instance = new SingleTon();
    }
    return this.#instance;
  }
}

const s1 = SingleTon.getInstance();
const s2 = SingleTon.getInstance();
console.log(s1 === s2); // true
  1. 发布订阅模式
  • 定义:发布订阅模式(Publish-Subscribe Pattern),也常被称为观察者模式(Observer Pattern),是一种设计模式,它创建了一种一对多的依赖关系,让多个订阅者对象能够自动接收到发布者对象发出的更新通知。这种模式的核心在于解耦,即发布者和订阅者之间不直接通信,而是通过一个中间层(也称为事件调度中心或消息通道)来间接通信。这样,当发布者有新事件发生时,只需通知这个中间层,由中间层再通知所有订阅了该事件的订阅者。
class PubSub {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  subscribe(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  // 发布事件
  publish(eventName, data) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      callbacks.forEach((callback) => callback(data));
    }
  }

  // 取消订阅
  unsubscribe(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        (cb) => cb !== callback
      );
    }
  }
}

// 订阅者函数1
const listener1 = (data) => console.log(`Listener 1 received: ${data}`);

// 订阅者函数2
const listener2 = (data) => console.log(`Listener 2 received: ${data}`);

// 创建PubSub实例
const pubsub = new PubSub();

// 订阅'news'事件
pubsub.subscribe("news", listener1);
pubsub.subscribe("news", listener2);

// 发布'news'事件,此时两个订阅者都会收到通知
document.querySelector(".publish").addEventListener("click", () => {
  pubsub.publish("news", "First news update.");
});

document.querySelector(".unsubscribe").addEventListener("click", () => {
  // 现在我们决定取消listener1的订阅
  pubsub.unsubscribe("news", listener1);
});

document.querySelector(".publish2").addEventListener("click", () => {
  // 再次发布'news'事件,此时只有listener2会收到通知
  pubsub.publish("news", "Second news update.");
});
  1. 原型模式
  • 定义:原型模式(Prototype Pattern)用于创建对象的副本,通过克隆已有对象来高效地生成新对象,而不是每次都重新创建相同的属性和方法。这种模式在 JavaScript 中尤为重要,因为 JavaScript 的对象模型本身就是基于原型的。
  • Object.create:将对象作为原型创建新对象。
let car = {
  color: "白色",
  run: function () {
    console.log("我会跑,我的名字是 " + this.name);
  },
};

let c1 = Object.create(car);
c1.name = "宝马";
c1.run();

let c2 = Object.create(car);
c2.name = "奔驰";
c2.run();

console.log(c1);
console.log(c2);
console.log(c1 === c2); // false
console.log(car === c1); // false
  • 适用场景
    1. 性能优化:当创建对象的成本较高(如涉及复杂计算、大量数据初始化或外部资源访问)时,克隆一个已经存在的对象比每次都执行构造函数更为高效。
    2. 避免重复初始化:对于需要多次创建相似对象的情况,原型模式可以避免每次创建时都执行相同的初始化逻辑。
    3. 代码复用:通过原型链可以实现方法和属性的共享,减少代码重复。
  1. 代理模式
  • 定义:用于在一个对象(称为目标对象)和它的访问者之间增加一层间接层,这层间接层就是代理对象。
// 目标对象
const target = {
  request: function () {
    console.log("模拟发送请求...");
  },
};

// 代理对象
const proxy = new Proxy(target, {
  get: function (receiver, property) {
    console.log(`Getting ${property}...`);
    return Reflect.get(...arguments);
  },
  apply: function (receiver, thisArg, argumentsList) {
    console.log("Calling function...");
    return Reflect.apply(...arguments);
  },
});

// 使用代理对象调用方法
proxy.request();
  • 适用场景
    1. 访问控制:代理可以用来检查调用者是否有权限访问目标对象的某些方法或属性。
    2. 缓存:代理可以缓存目标对象的结果,避免重复执行昂贵的操作,如网络请求、复杂计算等。

JavaScript 数据类型检测

  • typeof

底层机制:直接在计算机底层基于数据类型的值(二进制),进行检测。
优点:对基本数据类型友好。
缺点:typeof 检测普通对象/数组对象/日期对象/正则对象 都是 “object”。

console.log(typeof "abc"); // string
console.log(typeof 123); // number
  • instanceof(检测当前类型是否属于这个类)

底层机制:只要当前类出现在实例的原型链上,结果都为 true。
缺点:可以修改原型链,导致结果不准确。且不能检测基本数据类型。

console.log([] instanceof Array); // true
console.log([] instanceof RegExp); // false
console.log([] instanceof Object); // true
  • constructor

底层机制:通过调用对象的 constructor 属性,返回对象的类型。
缺点:constructor 可以修改,导致结果不准确。

console.log([].constructor === Array); // true
console.log([].constructor === Object); // false
console.log([].constructor === RegExp); // false
  • Object.prototype.toString.call()

底层机制:利用所有对象都继承自 Object.prototype 的特点,调用其上的 toString 方法。
是标准类型检测的方法,返回的结果是“[object 类型(Number/String/Boolean/Object/Array/Function/RegExp/Date)]”。

console.log(Object.prototype.toString.call(1)); // [object Number]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call([])); // [object Array]

从输入 URL 到页面呈现都发生了什么?

  1. 域名解析:确定协议、域名、路径和查询参数等信息。

  2. 缓存检查:检查本地缓存(内存缓存、硬盘缓存),如果请求的资源已经存在于缓存中且未过期,浏览器可能直接使用缓存内容而不进行后续网络请求

  3. DNS 解析:把域名翻译成 IP 地址,根据域名 端口 路径 找到对应资源。

  4. 建立 TCP 链接:浏览器通过 TCP 协议与服务器建立连接,这个过程包括三次握手,确保数据传输的可靠性(三次握手:发送响应->响应->接收响应)。

  5. 发送请求:包含 URL、浏览器信息、接受的内容类型等。

  6. 服务器处理并返回数据:包含状态码、响应头和响应体。

  7. 页面渲染:浏览器拿到数据,解析并渲染页面。

  8. 断开链接:断开 TCP 链接(四次挥手)。

浏览器缓存

  • 强缓存

    1. 发送请求之前,浏览器会自动检查缓存信息和缓存标识(不需要开发做什么)。
    2. 如果是第一次发请求,肯定没有标识和缓存,就会直接发请求。服务器会返回请求的结果和缓存标识。
      强缓存标识包括:
      2.1. Cache-Control
      2.2. Expires
      浏览器会自动把请求结果和缓存表示存储到浏览器中。
    3. 如果不是第一次发请求,且没有过期,就会直接从缓存中取数据,而不会发请求。

注意:

  1. HTML 页面一般不做缓存。
  2. 如果服务器文件更新,但是缓存不知道已经更新的情况下怎么处理?利用 webpack 的配置,让资源名称和之前不一样,这样页面倒入的就是全新的资源。
  3. 强缓存的优先级高于协商缓存。
  • 协商缓存

    1. 协商缓存就是没有缓存活着强缓存失效后,浏览器携带缓存标识向服务器发送请求(不管有没有缓存,都会携带缓存标识),由服务器根据缓存标识判断是否返回缓存数据。
      协商缓存标识包括:
      1.1. Last-Modified :服务器返回的资源最后修改时间
      1.2. ETag:服务器返回的资源唯一标识,每次资源更新都会重新生成
    2. 如果没更新,就返回 304,通知客户端读取缓存信息。
    3. 如果更新了,就返回 200,和新的资源,并且更新缓存标识。
    4. 第一次发送请求,本地没有任何标识,服务器会把内容+Last-Modified+ETag 都返回给浏览器。浏览器进行内容渲染和把信息、标识缓存到本地。
    5. 第二次发送请求,浏览器就会携带上标识再发送。(If-Modified-Since:Last-Modified;If-None-Match:ETag )
  • 强缓存和协商缓存的区别:
    协商缓存每次都会给服务器发送请求,进行协商,看下是否需要更新资源和标识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值