14 个 JavaScript 面试难题及代码实现

本文将深入剖析 14 个常见的 JavaScript 高级面试题。这些题目涵盖了 JavaScript 的面向对象、事件循环机制、Promise 等高级概念,以及函数柯里化、深拷贝等实用技巧。我们不仅从概念层面分析每一个问题,还提供具体的代码实现。

1. this关键字指向

this关键字指向当前执行上下文的一个对象。在一个函数内部,this关键字通常指向函数的调用者。✨

题目:下列代码输出什么?为什么?

const obj = {
  name: 'obj',
  getName: function() {
    return function() {
      return this.name;
    }
  }
}

const fn = obj.getName();
fn();

答案:undefined

分析:因为 getName 函数的内部函数是在全局作用域中执行的,这里的 this 指向 window/全局,而 window/全局没有 name 属性,所以返回 undefined。

如果要让内部函数的 this 也指向 obj,可以使用箭头函数或者 bind 来绑定 this:

const obj = {
  name: 'obj', 
  getName: function() {
    return () => {  
      return this.name;
    }
  }
}

2. 闭包的实现与应用

题目:实现一个计数器工厂函数:

function createCounter() {
  let count = 0;
  return function() {
    return count++; 
  }
}

const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // 1
counter1(); // 2
counter2(); // 1

分析:不同计数器可以独立递增的原因是利用了闭包的特性。createCounter 函数创建了一个可以访问其外层作用域变量 count 的闭包。counter1 和 counter2 引用了不同的闭包函数实例,从而实现了计数的独立性。

3. 事件循环机制

题目:给出事件循环机制的说明性注释。

答案:

事件循环机制主要有以下几个过程:

  1. 同步任务在主线程上执行,形成一个执行上下文栈。
  2. 一旦执行栈中的所有同步任务执行完毕,系统会读取异步任务队列中的任务,如 Promise.then()、setTimeout、AJAX 回调等。
  3. 异步任务会被添加到任务队列中。
  4. 一旦执行栈清空,系统会检查任务队列。如果不为空,则取出第一个任务放到执行栈上执行。
  5. 主线程重复执行栈和队列的交替执行过程,从而实现线程的队列化执行。

事件循环允许同步任务和异步任务在同一个线程中交替执行,从而充分利用 CPU 资源。这对支持 UI 交互和响应的 JavaScript 很重要。

4. Promise 对象

问题:实现一个简易版本的 Promise:

Promise 对象是异步编程中处理异步事件的一种解决方案。Promise 对象可以代表一个异步操作的状态,包括:

  • pending
  • fulfilled
  • rejected

Promise 对象的实现如下:

class MyPromise {
  constructor(executor) {
    this._state = "pending";
    this._value = undefined;
    this._reason = undefined;
    this._onFulfilledCallbacks = [];
    this._onRejectedCallbacks = [];

    executor(this.resolve.bind(this), this.reject.bind(this));
  }
  resolve(value) {
    if (this._state !== "pending") {
      return;
    }
    this._state = "fulfilled";
    this._value = value;
    setTimeout(() => {
      for (const callback of this._onFulfilledCallbacks) {
        callback(value);
      }
    });
  }
  reject(reason) {
    if (this._state !== "pending") {
      return;
    }
    this._state = "rejected";
    this._reason = reason;
    setTimeout(() => {
      for (const callback of this._onRejectedCallbacks) {
        callback(reason);
      }
    });
  }
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this._state === "pending") {
        this._onFulfilledCallbacks.push((value) => {
          setTimeout(() => {
            try {
              const result = onFulfilled(value);
              resolve(result);
            } catch (error) {
              reject(error);
            }
          });
        });
        this._onRejectedCallbacks.push((reason) => {
          setTimeout(() => {
            try {
              const result = onRejected(reason);
              resolve(result);
            } catch (error) {
              reject(error);
            }
          });
        });
      } else {
        setTimeout(() => {
          try {
            if (this._state === "fulfilled") {
              const result = onFulfilled(this._value);
              resolve(result);
            } else {
              const result = onRejected(this._reason);
              resolve(result);
            }
          } catch (error) {
            reject(error);
          }
        });
      }
    });
  }
  catch(onRejected) {
    return this.then(null, onRejected);
  }
  isFulfilled() {
    return this._state === "fulfilled";
  }
  isRejected() {
    return this._state === "rejected";
  }
}

分析:

  1. ️ MyPromise 类是一个自定义的 Promise 类,其构造函数接收一个执行器函数作为参数。
  2. 执行器函数会在构造函数中立即执行,并传入 resolve、reject 两个参数,用于修改 Promise 的状态。
  3. resolve 方法用于将 Promise 的状态从“未完成”修改为“已完成”,并将值传给后续的处理程序。
  4. reject 方法用于将 Promise 的状态从“未完成”修改为“已拒绝”,并将原因传给后续处理程序。
  5. then 方法用于注册 Promise 完成或被拒绝时要执行的回调函数,它接受 onFulfilled、onRejected 两个参数,分别在 Promise 完成或被拒绝时调用。

5. 类继承的实现

原型链是每个对象都具有的一个属性,指向该对象的构造函数的原型对象。某个构造函数的原型对象又指向另一个构造函数的原型对象,以此类推。

题目:实现一个 People 类,可通过构造函数或 new 运算符进行对象实例化。同时继承 Person 类,Person 类具有 sayHi 方法:

class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHi() {
    console.log(`Hello ${this.name}`)
  }
}

class People extends Person {
  constructor(name) {
    super(name);
  }
  method() {
    console.log('people method')
  }
}
const people = new People('John')
people.sayHi() // Hello John
people.method() // people method

分析:通过构造函数 super 调用继承属性,通过原型链实现方法继承。

6. MVC 和 MVVM 模式

题目:简要描述 MVC 和 MVVM 的概念及差异?

答案:

在 MVC 模式中:

  • Model 负责管理数据逻辑
  • View 负责显示界面
  • Controller 连接 Model 和 View,传递数据

在 MVVM 模式中:

  • Model 负责管理数据逻辑
  • View 负责显示界面
  • ViewModel 充当 View 和 Model 之间的互动代理,将模型同步到视图,将视图的更改同步回模型。

两者的差异在于:

  • MVVM 中没有 Controller 角色,View 直接绑定数据到 ViewModel。
  • ViewModel 负责将数据转换成 View 可以识别的格式并提供给 View 使用。
  • ViewModel 可以将视图中的更改通知回 Model 层,实现双向数据绑定。
  • MVVM 可以解耦 View 和 Model 的紧密耦合,有利于单元测试和组件化开发。

7. Ajax 实现

题目:实现一个 ajax 请求函数:

ajax('/api/users', {
  method: 'GET'   
})
.then(data => {
  console.log(data) 
})

答案:

function ajax(url, options) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const method = options.method || 'GET';
    const headers = options.headers || {};
    const body = options.body || null;
    const timeout = options.timeout || 0;

    xhr.open(method, url);
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject(new Error(xhr.statusText));
      }
    };
    xhr.onerror = () => reject(xhr.error);
    xhr.ontimeout = () => reject(new Error('Request timeout'));
    xhr.timeout = timeout;
    for (const [header, value] of Object.entries(headers)) {
      xhr.setRequestHeader(header, value);
    }
    xhr.send(body);
  });
}

分析:使用 Promise 封装异步 ajax 请求,实现同步编程方式。

8. JSONP 跨域实现

题目:实现一个 JSONP 跨域请求:

jsonp('/api/data', {
  params: {
    name: 'jsonp'
  }   
})

答案:

function jsonp(url, options) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    const callbackName = `jsonpCallback_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
    const timer = setTimeout(() => {
      cleanup();
      reject(new Error('JSONP request timeout'));
    }, options.timeout || 5000);
    function cleanup() {
      delete window[callbackName];
      clearTimeout(timer);
      script.remove();
    }
    window[callbackName] = function(data) {
      cleanup();
      resolve(data);
    };
    options.params = options.params || {};
    options.params['callback'] = callbackName;
    const paramsArr = Object.keys(options.params).map(key => {
      return `${encodeURIComponent(key)}=${encodeURIComponent(options.params[key])}`;
    });
    script.src = `${url}?${paramsArr.join('&')}`;
    script.onerror = () => {
      cleanup();
      reject(new Error('JSONP request error'));
    };
    document.body.appendChild(script);
  });
}

分析:创建 script 节点,设置回调函数,解析参数拼接 URL,动态插入 body 实现 JSONP 跨域请求,返回 Promise 接口。

9. 实现深拷贝

题目:实现一个 deepClone 函数实现对象的深拷贝:

function deepClone(source, clonedMap) {
  clonedMap = clonedMap || new Map();

  if (source === null || typeof source !== 'object') {
    return source;
  }
  if (clonedMap.has(source)) {
    return clonedMap.get(source);
  }
  var result;
  var type = getType(source);
  if (type === 'object' || type === 'array') {
    result = type === 'array' ? [] : {};
    clonedMap.set(source, result);
    for (var key in source) {
      if (source.hasOwnProperty(key)) {
        result[key] = deepClone(source[key], clonedMap);
      }
    }
  } else {
    result = source;
  }
  return result;
}
function getType(source) {
  return Object.prototype.toString
    .call(source)
    .replace(/^[object (.+)]$/, '$1')
    .toLowerCase();
}
const obj = {
  a: 1,
  b: {
    c: 2
  }
}
const clone = deepClone(obj)

分析:递归实现对象与数组的深拷贝,基本类型直接返回,引用类型递归层层调用深拷贝。

10. 函数柯里化

题目:实现一个 add 函数,可以实现 1+2+3 的相加:

function add() {
     // When executing for the first time, define an array specifically to store all parameters
     var _args = [].slice.call(arguments);

     // Declare a function internally, use the characteristics of closure to save _args and collect all parameter values
     var adder = function () {
         var _adder = function() {
             // [].push.apply(_args, [].slice.call(arguments));
             _args.push(...arguments);
             return _adder;
         };
         //Using the characteristics of implicit conversion, implicit conversion is performed when it is finally executed, and the final value is calculated and returned.
         _adder.toString = function () {
             return _args.reduce(function (a, b) {
                 return a + b;
             });
         }
         return _adder;
     }
     // return adder.apply(null, _args);
     return adder(..._args);
}
var a = add(1)(2)(3)(4); // f 10
var b = add(1, 2, 3, 4); // f 10
var c = add(1, 2)(3, 4); // f 10
var d = add(1, 2, 3)(4); // f 10
// You can use the characteristics of implicit conversion to participate in calculations
console.log(a + 10); // 20
console.log(b + 20); // 30
console.log(c + 30); // 40
console.log(d + 40); // 50
// You can also continue to pass in parameters, and the result will be calculated using implicit conversion again.
console.log(a(10) + 100); // 120
console.log(b(10) + 100); // 120
console.log(c(10) + 100); // 120
console.log(d(10) + 100); // 120
//In fact, the add method in Shangli is the curried function of the following function, but we do not use the general formula to convert, but encapsulate it ourselves.
function add(...args) {
     return args.reduce((a, b) => a + b);
}

分析:add 函数的柯里化是通过递归调用一个不断接收参数的函数来实现的。

12. 实现 promise.all 方法

题目:实现一个 myAll 方法,类似于 Promise.all:

myAll([
  myPromise1,
  myPromise2 
]).then(([res1, res2]) => {
  // ...
})

答案:

function myAll(promises) {
  return new Promise((resolve, reject) => {
    const result = new Array(promises.length);
    let count = 0;
    promises.forEach((p, index) => {
      p.then(res => {
        result[index] = res;
        count++;
        if (count === promises.length) {
          resolve(result);
        }
      })
      .catch(reject);
    });
  });
}

分析:利用 Promise.all 的原理通过计数器和结果数组同步 Promise 状态。

13. 实现 instanceof

题目:实现一个 instanceof 操作符

答案:

function instanceof(left, right) {
  if (arguments.length !== 2) {
    throw new Error("instanceof requires exactly two arguments.");
  }
  if (left === null) {
    return false;
  }
  if (typeof left !== "object") {
    return false;
  }
  let proto = Object.getPrototypeOf(left);
  while (proto !== null) {
    if (right.prototype === proto) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

上述代码用于判断一个对象是否为另一个对象的实例。

JavaScript 中的 instanceof 操作符可以用来判断一个对象是否为另一个对象的实例。但是 instanceof 操作符也存在一些限制,如:

  • 仅可判断直接相连的原型链上的对象
  • 无法检测循环原型链上的对象

因此,上述代码提供了一个更一般的 instanceof 函数,可以判断任意两个对象之间的关系。

该函数的实现原理是:

  1. 函数接受左右两个参数 left 和 right
  2. 首先检查参数个数是否为 2,否则抛出错误
  3. 接下来检查左操作数 left 是否为 null,是则直接返回 false
  4. 再检查左操作数类型是否为对象,非对象则直接返回 false
  5. 通过 Object.getPrototypeOf() 获得左操作数的原型,赋值给变量 proto
  6. 在一个循环内遍历 proto 的原型链,直到 proto 为 null
  7. 循环内判断右操作数的原型是否与当前 proto 相等,相等则返回 true
  8. 最终若未找到匹配原型,则返回 false

这个函数可用于以下场景:

  • 判断一个对象是否为另一个对象的实例
  • 判断一个对象是否继承自另一个对象
  • 判断一个对象是否属于某个具体的类

14. 实现防抖函数

题目:实现一个防抖函数

答案:

function debounce(fn, delay = 500) {
   let timer;

   return function(...args) {
     clearTimeout(timer);
     timer = setTimeout(() => {
       fn.apply(this, args);
     }, delay);
     // Return to clear function
     return () => {
       clearTimeout(timer);
     }
   }
}
// use
const debouncedFn = debounce(fn, 1000);
const cancel = debouncedFn();
// clear
cancel();

一定要 点赞关注 我哟️

你可能感兴趣的:(javascript,面试,okhttp)