Skip to content

JavaScript 高级特性

原型与原型链

new 操作符的实现

new 操作符具体干了什么?

  1. 创建一个空对象,将它的引用赋给 this,继承函数的原型
  2. 通过 this 将属性和方法添加至这个对象
  3. 最后返回 this 指向的新对象,也就是实例(如果没有手动返回其他的对象)

手写 new 实现:

javascript
function myNew(constructor, ...args) {
  // 1. 创建一个空对象,原型指向构造函数的 prototype
  const obj = Object.create(constructor.prototype);
  
  // 2. 执行构造函数,绑定 this
  const result = constructor.apply(obj, args);
  
  // 3. 如果构造函数返回了对象,则返回该对象,否则返回新创建的对象
  return result instanceof Object ? result : obj;
}

// 使用示例
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person = myNew(Person, '张三', 25);
console.log(person); // Person { name: '张三', age: 25 }

instanceof 原理

原理: 通过判断对象的原型链中是否存在指定构造函数的原型。

主要用于判断引用类型的数据,原始数据类型(如字符串、数字、布尔值)则无法准确判断。

手写 instanceof:

javascript
function myInstanceof(obj, constructor) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(obj);
  
  // 获取构造函数的 prototype
  const prototype = constructor.prototype;
  
  // 沿着原型链查找
  while (proto) {
    if (proto === prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  
  return false;
}

// 使用示例
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof([], Object)); // true
console.log(myInstanceof({}, Array)); // false

字面量创建对象和 new 创建对象的区别

字面量创建:

javascript
const obj = { name: '张三' };
// 原型链:obj -> Object.prototype -> null

new 创建:

javascript
function Person(name) {
  this.name = name;
}
const obj = new Person('张三');
// 原型链:obj -> Person.prototype -> Object.prototype -> null

Object.create(null) 创建:

javascript
const obj = Object.create(null);
obj.name = '张三';
// 原型链:obj -> null(没有原型链)

区别:

  • 字面量和 new 出来的对象有原型链,可以访问继承的属性和方法
  • Object.create(null) 创建的对象没有原型链,无法访问任何继承的属性或方法
  • 适用于创建纯净的对象,作为字典使用

作用域与闭包

闭包

什么是闭包?

闭包是指有权访问另一个函数作用域中变量的函数。

应用场景:

  1. 模块模式(私有变量)
javascript
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    getCount: () => count
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.getCount()); // 1
  1. 函数柯里化
javascript
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

function sum(a, b, c) {
  return a + b + c;
}

const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
  1. 防抖和节流
javascript
// 防抖
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 节流
function throttle(fn, delay) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

异步编程

宏任务和微任务

宏任务(Macro Task):

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate(Node.js)
  • I/O
  • UI Rendering

微任务(Micro Task):

  • Promise
  • process.nextTick(Node.js)
  • MutationObserver

执行顺序:

  1. 执行宏任务 script
  2. 进入 script 后,所有的同步任务主线程执行
  3. 所有宏任务放入宏任务执行队列
  4. 所有微任务放入微任务执行队列
  5. 先清空微任务队列
  6. 再取一个宏任务,执行,再清空微任务队列
  7. 依次循环

示例:

javascript
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

// 输出顺序:1, 4, 3, 2

Promise 实现

手写 Promise:

javascript
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    
    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    };
    
    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };
    
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
    
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);
      }
      
      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);
      }
      
      if (this.state === 'pending') {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
        
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
      }
    });
    
    return promise2;
  }
  
  resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise'));
    }
    
    if (x instanceof MyPromise) {
      x.then(resolve, reject);
    } else {
      resolve(x);
    }
  }
}

设计模式

单例模式

javascript
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
  }
}

const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

观察者模式

javascript
class Subject {
  constructor() {
    this.observers = [];
  }
  
  addObserver(observer) {
    this.observers.push(observer);
  }
  
  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log('Received data:', data);
  }
}

// 使用示例
const subject = new Subject();
const observer = new Observer();
subject.addObserver(observer);
subject.notify('Hello'); // Received data: Hello

发布订阅模式

javascript
class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }
  
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(...args));
    }
  }
  
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
}

// 使用示例
const emitter = new EventEmitter();
const callback = (data) => console.log('Received:', data);
emitter.on('message', callback);
emitter.emit('message', 'Hello'); // Received: Hello

其他重要概念

requestAnimationFrame

什么是 requestAnimationFrame?

requestAnimationFrame 请求数据帧可以用做动画执行。

特点:

  • 可以自己决定什么时机调用该回调函数
  • 能保证每次屏幕刷新的时候只被执行一次
  • 页面被隐藏或者最小化的时候暂停执行,返回窗口继续执行,有效节省 CPU

使用示例:

javascript
function animate() {
  // 动画逻辑
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

URL 编码

encodeURIComponent 和 encodeURI 的区别:

  • encodeURI:用于处理整个 URI,不会转义 &, ?, /, = 等功能字符
  • encodeURIComponent:用于编码 URI 中的值,会转义所有特殊字符
javascript
const url = 'https://example.com?name=张三&age=25';

console.log(encodeURI(url));
// https://example.com?name=%E5%BC%A0%E4%B8%89&age=25

console.log(encodeURIComponent(url));
// https%3A%2F%2Fexample.com%3Fname%3D%E5%BC%A0%E4%B8%89%26age%3D25

为什么需要编码?

URL 地址携带的参数经过编码是为了确保参数值中的特殊字符不会影响 URL 的解析和传输。特殊字符如空格、&#% 等在 URL 中具有特殊含义,如果不经过编码,可能会导致 URL 解析错误或者参数传递错误。