Skip to content

JavaScript 原型与作用域链详解

1. 原型(Prototype)

1.1 原型的基本概念

在 JavaScript 中,每个对象都有一个原型(prototype)属性,它指向另一个对象。这个原型对象又有自己的原型,形成了一个原型链(prototype chain)。当我们访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript 会沿着原型链向上查找,直到找到该属性或到达原型链的末端(null)。

1.2 原型的作用

  • 继承:通过原型链实现对象之间的继承关系
  • 代码复用:可以在原型上定义方法,所有实例共享这些方法
  • 属性查找:当访问对象属性时,会沿着原型链查找

1.3 原型的创建方式

1.3.1 使用构造函数

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 在原型上定义方法
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

// 创建实例
const person1 = new Person('Alice', 25);
const person2 = new Person('Bob', 30);

person1.sayHello(); // Hello, my name is Alice
person2.sayHello(); // Hello, my name is Bob

1.3.2 使用 Object.create()

javascript
// 创建原型对象
const animalProto = {
  eat: function() {
    console.log('Eating...');
  }
};

// 创建继承自animalProto的对象
const cat = Object.create(animalProto);
cat.name = 'Kitty';
cat.meow = function() {
  console.log('Meow!');
};

cat.eat(); // Eating...
cat.meow(); // Meow!

1.3.3 使用 ES6 类

javascript
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name} is barking`);
  }
}

const dog = new Dog('Buddy', 'Labrador');
dog.eat(); // Buddy is eating
dog.bark(); // Buddy is barking

1.4 原型链

原型链是 JavaScript 实现继承的核心机制。当访问一个对象的属性时,JavaScript 会:

  1. 首先检查对象本身是否有该属性
  2. 如果没有,就检查对象的原型
  3. 如果原型也没有,就检查原型的原型
  4. 直到找到该属性或到达原型链的末端(null)
javascript
// 原型链示例
function Grandparent() {
  this.grandparentProp = 'grandparent';
}

function Parent() {
  this.parentProp = 'parent';
}

// 继承Grandparent
Parent.prototype = new Grandparent();

function Child() {
  this.childProp = 'child';
}

// 继承Parent
Child.prototype = new Parent();

const child = new Child();
console.log(child.childProp); // child
console.log(child.parentProp); // parent
console.log(child.grandparentProp); // grandparent
console.log(child.toString()); // [object Object] (来自Object.prototype)

2. 作用域(Scope)

2.1 作用域的基本概念

作用域是指变量、函数和对象的可访问范围。在 JavaScript 中,作用域决定了代码块中变量的可见性和生命周期。

2.2 作用域的类型

2.2.1 全局作用域

在任何函数外部声明的变量都属于全局作用域,可以在代码的任何地方访问。

javascript
const globalVar = 'global';

function test() {
  console.log(globalVar); // 可以访问全局变量
}

test(); // global

2.2.2 函数作用域

在函数内部声明的变量属于函数作用域,只能在函数内部访问。

javascript
function test() {
  const localVar = 'local';
  console.log(localVar); // 可以访问局部变量
}

test(); // local
console.log(localVar); // 错误:localVar is not defined

2.2.3 块级作用域

在 ES6 中,使用 letconst 声明的变量属于块级作用域,只能在声明它们的块(如 if、for、while 等)内部访问。

javascript
if (true) {
  let blockVar = 'block';
  console.log(blockVar); // block
}

console.log(blockVar); // 错误:blockVar is not defined

3. 作用域链(Scope Chain)

3.1 作用域链的基本概念

作用域链是指当访问一个变量时,JavaScript 会从当前作用域开始查找,如果找不到,就向上级作用域查找,直到找到该变量或到达全局作用域。

3.2 作用域链的工作原理

当函数被创建时,它会创建一个作用域链,包含了函数自身的变量对象和所有父级作用域的变量对象。当函数执行时,它会使用这个作用域链来查找变量。

javascript
const globalVar = 'global';

function outer() {
  const outerVar = 'outer';
  
  function inner() {
    const innerVar = 'inner';
    console.log(innerVar); // inner (当前作用域)
    console.log(outerVar); // outer (上级作用域)
    console.log(globalVar); // global (全局作用域)
  }
  
  inner();
}

outer();

3.3 闭包与作用域链

闭包是指有权访问另一个函数作用域中变量的函数。闭包的形成与作用域链密切相关。

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

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

在这个例子中,createCounter 函数返回一个内部函数,这个内部函数形成了一个闭包,它可以访问 createCounter 函数作用域中的 count 变量,即使 createCounter 函数已经执行完毕。

4. 原型与作用域链的关系

原型链和作用域链是 JavaScript 中两个不同的概念,但它们都与查找机制有关:

  • 原型链:用于查找对象的属性和方法
  • 作用域链:用于查找变量和函数

4.1 示例:原型链与作用域链的结合

javascript
// 全局作用域
const globalVar = 'global';

// 构造函数
function Person(name) {
  // 函数作用域
  const localVar = 'local';
  this.name = name;
  
  this.sayHello = function() {
    // 函数作用域
    console.log(`Hello, my name is ${this.name}`);
    console.log(localVar); // 访问函数作用域变量
    console.log(globalVar); // 访问全局作用域变量
  };
}

// 在原型上定义方法
Person.prototype.sayGoodbye = function() {
  console.log(`Goodbye, ${this.name}`);
  // console.log(localVar); // 错误:localVar is not defined (无法访问构造函数作用域)
  console.log(globalVar); // 可以访问全局作用域变量
};

const person = new Person('Alice');
person.sayHello(); // Hello, my name is Alice; local; global
person.sayGoodbye(); // Goodbye, Alice; global

5. 常见问题与解决方案

5.1 原型链查找的性能问题

当原型链过长时,属性查找的性能会受到影响。解决方案:

  • 避免过深的原型链
  • 对于频繁访问的属性,可以将其复制到对象本身

5.2 作用域链的内存泄漏

闭包可能导致内存泄漏,因为闭包会保持对外部作用域变量的引用。解决方案:

  • 避免不必要的闭包
  • 在不再需要时,手动解除引用

5.3 原型污染

修改共享原型可能会影响所有实例。解决方案:

  • 避免直接修改内置对象的原型
  • 使用 Object.create() 创建隔离的原型

6. 实战案例

6.1 原型继承的最佳实践

javascript
// 方法1:使用 Object.create()
const animalProto = {
  eat() {
    console.log('Eating...');
  }
};

const dogProto = Object.create(animalProto);
dogProto.bark = function() {
  console.log('Barking...');
};

const dog = Object.create(dogProto);
dog.name = 'Buddy';

// 方法2:使用 ES6 类
class Animal {
  eat() {
    console.log('Eating...');
  }
}

class Dog extends Animal {
  bark() {
    console.log('Barking...');
  }
}

const dog2 = new Dog();
dog2.name = 'Max';

6.2 作用域链的应用

javascript
// 模块模式
const module = (function() {
  // 私有变量
  let privateVar = 'private';
  
  // 公共方法
  return {
    getPrivateVar() {
      return privateVar;
    },
    setPrivateVar(value) {
      privateVar = value;
    }
  };
})();

console.log(module.getPrivateVar()); // private
module.setPrivateVar('modified');
console.log(module.getPrivateVar()); // modified
console.log(privateVar); // 错误:privateVar is not defined

6.3 原型与作用域链的综合应用

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

// 继承 EventEmitter
class Logger extends EventEmitter {
  log(message) {
    console.log(`[${new Date().toISOString()}] ${message}`);
    this.emit('log', message);
  }
}

// 使用
const logger = new Logger();

// 监听日志事件
logger.on('log', (message) => {
  console.log(`Event received: ${message}`);
});

logger.log('Hello, world!');
// 输出:
// [2023-10-01T00:00:00.000Z] Hello, world!
// Event received: Hello, world!

7. 总结

  • 原型:JavaScript 对象的继承机制,通过原型链实现属性和方法的共享
  • 作用域:变量的可访问范围,包括全局作用域、函数作用域和块级作用域
  • 原型链:对象属性查找的机制,从对象本身开始,沿着原型链向上查找
  • 作用域链:变量查找的机制,从当前作用域开始,沿着作用域链向上查找

理解原型和作用域链是掌握 JavaScript 核心概念的关键,它们对于编写高效、可维护的代码至关重要。

基于 VitePress 的本地知识库