14个JS面试难点深入解读与代码实现

大家好,我是宝哥

现如今,JavaScript已成为前端开发必不可少的核心技术。而随着单页应用(SPA)的兴起,JavaScript也从原来的前端脚本语言发展成了真正意义上的前端开发语言。掌握JavaScript高级技巧已成为每一位前端工程师的必备技能。

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

1.this关键字指向

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

问题:下面代码输出什么?为什么?

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

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

答: undefined

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

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

const obj = {
  name'obj',
  getNamefunction() {
    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. 同步任务都在主线程执行,形成一个执行栈(execution context stack)
  2. 一旦执行栈中的所有同步任务执行完毕,系统就会读取队列中的异步任务,如 Promise.then()、setTimeout、AJAX回调等
  3. 异步任务将被添加到任务队列(task queue)中
  4. 一旦执行栈 cleared(清空),系统就检查任务队列,如果不为空就把第一个任务从中取出放到执行栈中执行
  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 类,它的构造函数接受一个 executor 函数作为参数。
  2. 构造函数中的 executor 函数会立即执行,并接受两个参数 resolve 和 reject,用于修改 Promise 的状态。
  3. resolve 方法用于将 Promise 的状态从 “pending” 修改为 “fulfilled”,并将值传递给后续的处理程序。
  4. reject 方法用于将 Promise 的状态从 “pending” 修改为 “rejected”,并将原因传递给后续的处理程序。
  5. then 方法用于注册在 Promise 完成或拒绝时执行的回调函数。它接受两个参数:onFulfilled 和 onRejected,它们分别在 Promise 完成或拒绝时被调用。
  6. then 方法返回一个新的 MyPromise 实例,以支持链式调用。如果 onFulfilled 或 onRejected 返回一个值,它将被用作下一个 MyPromise 实例的解析值。
  7. catch 方法是 then(null, onRejected) 的简写形式。
  8. isFulfilled 方法用于检查 Promise 是否处于已完成状态。
  9. isRejected 方法用于检查 Promise 是否处于已拒绝状态。

5.Class继承实现

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

问题:实现一个People类,可以通过构造函数或新操作符来实例化对象,同时它有一个method方法,继承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

解析:通过constructor调用super继承属性,原型链实现方法的继承。

7.MVC和MVVM模式

问题:简要描述MVC与MVVM的概念及区别?

答:

MVC模式中:

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

MVVM模式中:

  • Model负责管理数据逻辑
  • View负责展示界面
  • ViewModel作为View和Model的交互代理,将模型同步到视图,同时将视图变化同步回模型。

区别在于:

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

8.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请求函数:

  • 使用 XMLHttpRequest 对象发送请求
  • 初始化open方法、配置请求方法和url
  • 添加onload和onerror回调函数
  • onload判断状态码是否在200-300范围内resolve,否则reject
  • onerror直接reject
  • 在请求成功后resolve返回response,失败后reject报错
  • 支持options配置请求参数和请求体
  • 返回一个Promise对象,外部可以使用then/catch处理

解析:利用Promise封装了异步ajax请求,实现了同步的编程风格。

9.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节点 script.src 设置回调函数callbackName解析参数拼接url,动态插入body中实现JSONP跨域请求,返回Promise接口。

10.实现深度克隆

问题:实现一个函数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 = {
  a1,
  b: {
    c2
  }
}

const clone = deepClone(obj)

解析:递归实现对象和数组的深度克隆,对基础类型直接返回,引用类型按层次递归调用深度克隆。

11.函数柯里化

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

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = [].slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var adder = function () {
        var _adder = function() {
            // [].push.apply(_args, [].slice.call(arguments));
            _args.push(...arguments);
            return _adder;
        };

        // 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
        _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(1234);   // f 10
var c = add(12)(34);   // f 10
var d = add(123)(4);   // f 10

// 可以利用隐式转换的特性参与计算
console.log(a + 10); // 20
console.log(b + 20); // 30
console.log(c + 30); // 40
console.log(d + 40); // 50

// 也可以继续传入参数,得到的结果再次利用隐式转换参与计算
console.log(a(10) + 100);  // 120
console.log(b(10) + 100);  // 120
console.log(c(10) + 100);  // 120
console.log(d(10) + 100);  // 120
// 其实上栗中的add方法,就是下面这个函数的柯里化函数,只不过我们并没有使用通用式来转化,而是自己封装
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 运算符只能判断原型链直接相连的对象。
  • instanceof 运算符不能判断存在循环原型链的对象。

因此,上面这段代码提供了一种更通用的 instanceof 函数,可以判断任意两个对象的关系。

该函数的实现原理是:

  1. 函数 instanceof 接收两个参数:left 和 right。
  2. 首先,代码检查参数个数是否为 2,如果不是,则抛出一个错误。
  3. 接下来,代码检查左操作数 left 是否为 null,如果是,则直接返回 false,因为 null 不可能是任何对象的实例。
  4. 然后,代码检查左操作数 left 的类型是否为对象,如果不是,则直接返回 false,因为只有对象才可能是构造函数的实例。
  5. 接着,代码使用 Object.getPrototypeOf() 获取左操作数 left 的原型,并将其赋值给变量 proto。
  6. 在一个循环中,代码不断遍历 proto 的原型链,直到 proto 为 null。
  7. 在循环中,代码检查右操作数 right 的原型是否与当前 proto 相等,如果相等,则说明左操作数 left 是右操作数 right 的实例,返回 true。
  8. 如果循环结束仍未找到匹配的原型,即 proto 为 null,则说明左操作数 left 不是右操作数 right 的实例,返回 false。

该函数可以用于以下场景:

  • 判断一个对象是否是另一个对象的实例。
  • 判断一个对象是否继承自另一个对象。
  • 判断一个对象是否属于一个特定的类。

14.实现一个 debounce防抖函数

问题:实现一个debounce函数

答:

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

  return function(...args{
    clearTimeout(timer);

    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);

    // 返回清除函数
    return () => {
      clearTimeout(timer);
    }
  }
}

// 使用
const debouncedFn = debounce(fn, 1000);
const cancel = debouncedFn();

// 清除
cancel();

-END-


关注我

我的微信公众号:前端开发博客,在后台回复以下关键字可以获取资源。

  • 回复「小抄」,领取Vue、JavaScript 和 WebComponent 小抄 PDF
  • 回复「Vue脑图」获取 Vue 相关脑图
  • 回复「思维图」获取 JavaScript 相关思维图
  • 回复「简历」获取简历制作建议
  • 回复「简历模板」获取精选的简历模板
  • 回复「加群」进入500人前端精英群
  • 回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
  • 回复「知识点」下载高清JavaScript知识点图谱

每日分享有用的前端开发知识,加我微信:caibaojian89 交流