Project-Ideas:前端原型链与继承实现

Project-Ideas:前端原型链与继承实现

【免费下载链接】Project-Ideas-And-Resources A Collection of application ideas that can be used to improve your coding skills ❤. 【免费下载链接】Project-Ideas-And-Resources 项目地址: https://gitcode.com/GitHub_Trending/pr/Project-Ideas-And-Resources

你还在为这些问题烦恼吗?

  • 为什么 [] instanceof Array 返回 true 而 [] instanceof Object 也返回 true?
  • 为什么构造函数里的 this 能指向实例对象?
  • 原型链继承、class 继承、组合继承到底有什么区别?
  • 为什么说 Object.prototype.__proto__ 是原型链的尽头?

读完你能得到

  • 原型链(Prototype Chain)的底层工作原理
  • 6种继承方式的实现代码与优缺点对比
  • 原型污染的成因与防御措施
  • 继承在React/Vue组件开发中的最佳实践
  • 10道大厂面试题解析与避坑指南

一、JavaScript对象模型核心:原型链详解

1.1 什么是原型(Prototype)

在JavaScript中,每个对象都有一个特殊的内部属性[[Prototype]](可通过Object.getPrototypeOf()访问),这个属性指向另一个对象,这就是原型。当访问对象的属性时,如果对象本身没有该属性,JavaScript引擎会沿着原型链向上查找,直到找到该属性或到达原型链的终点。

// 创建一个普通对象
const obj = { name: '原型示例' };
// 获取对象的原型
const proto = Object.getPrototypeOf(obj);
// 原型链终点
console.log(Object.getPrototypeOf(Object.prototype)); // null

1.2 原型链的工作流程图

mermaid

1.3 构造函数与原型的关系

每个构造函数都有一个prototype属性,指向实例对象的原型。当使用new关键字创建实例时,实例的[[Prototype]]会指向构造函数的prototype属性。

// 构造函数
function Person(name) {
  this.name = name;
}
// 构造函数的prototype属性
Person.prototype.sayHello = function() {
  console.log(`Hello, ${this.name}`);
};
// 创建实例
const person = new Person('张三');
// 实例的原型就是构造函数的prototype
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
// 原型的constructor属性指回构造函数
console.log(Person.prototype.constructor === Person); // true

1.4 原型链查找规则

  1. 当访问对象属性时,先检查对象本身是否存在该属性
  2. 如果不存在,就查找对象的原型
  3. 以此类推,直到找到属性或到达原型链终点(null)
  4. 如果整个原型链都找不到该属性,则返回undefined
const obj = { a: 1 };
// 给原型添加属性
Object.prototype.b = 2;
// 查找a:obj自身有a属性
console.log(obj.a); // 1
// 查找b:obj自身没有,在原型上找到
console.log(obj.b); // 2
// 查找c:整个原型链都没有
console.log(obj.c); // undefined

二、JavaScript继承的6种实现方式

2.1 原型链继承

实现原理:让子类的原型对象指向父类的实例,从而继承父类的属性和方法。

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 子类
function Dog(name, age) {
  this.age = age;
}

// 关键:让Dog的原型指向Animal的实例
Dog.prototype = new Animal();
// 修复constructor指向
Dog.prototype.constructor = Dog;

// 添加子类特有方法
Dog.prototype.bark = function() {
  console.log('Woof! Woof!');
};

// 测试
const dog1 = new Dog('小黑', 2);
const dog2 = new Dog('小白', 3);

dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white', 'brown'] (引用类型共享问题)

优缺点: | 优点 | 缺点 | |------|------| | 实现简单,易于理解 | 父类的引用类型属性会被所有子类实例共享 | | 可以继承父类原型上的方法 | 创建子类实例时,无法向父类构造函数传递参数 |

2.2 构造函数继承

实现原理:在子类构造函数中调用父类构造函数,通过call/apply改变this指向,实现属性继承。

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 子类
function Dog(name, age) {
  // 关键:调用父类构造函数
  Animal.call(this, name);
  this.age = age;
}

// 测试
const dog1 = new Dog('小黑', 2);
const dog2 = new Dog('小白', 3);

dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'] (解决引用类型共享问题)
// 无法继承原型上的方法
console.log(dog1.eat); // undefined

优缺点: | 优点 | 缺点 | |------|------| | 解决了引用类型属性共享问题 | 无法继承父类原型上的方法 | | 可以向父类构造函数传递参数 | 每个实例都会拥有父类方法的副本,浪费内存 |

2.3 组合继承

实现原理:结合原型链继承和构造函数继承的优点,使用原型链继承原型上的方法,使用构造函数继承实例属性。

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 子类
function Dog(name, age) {
  // 构造函数继承:继承实例属性
  Animal.call(this, name);
  this.age = age;
}

// 原型链继承:继承原型方法
Dog.prototype = new Animal();
// 修复constructor指向
Dog.prototype.constructor = Dog;

// 添加子类特有方法
Dog.prototype.bark = function() {
  console.log('Woof! Woof!');
};

// 测试
const dog = new Dog('小黑', 2);
dog.eat(); // 小黑 is eating
dog.bark(); // Woof! Woof!
console.log(dog.name); // 小黑

优缺点: | 优点 | 缺点 | |------|------| | 既可以继承实例属性,又可以继承原型方法 | 父类构造函数被调用两次(一次创建子类原型,一次在子类构造函数中) | | 避免了引用类型属性共享问题 | 子类原型上会有父类的实例属性,造成内存浪费 |

2.4 原型式继承

实现原理:创建一个临时构造函数,将传入的对象作为这个构造函数的原型,然后返回这个构造函数的实例。

// 实现原型式继承的函数
function objectCreate(proto) {
  function F() {};
  F.prototype = proto;
  return new F();
}

// 等同于ES5的Object.create()

// 使用示例
const animal = {
  name: '动物',
  colors: ['black', 'white'],
  eat: function() {
    console.log(`${this.name} is eating`);
  }
};

// 创建继承自animal的对象
const dog = objectCreate(animal);
dog.name = '狗';
dog.bark = function() {
  console.log('Woof! Woof!');
};

console.log(dog.name); // 狗
dog.eat(); // 狗 is eating
dog.bark(); // Woof! Woof!

优缺点: | 优点 | 缺点 | |------|------| | 简单易用,适合创建对象的副本 | 引用类型属性仍然会被共享 | | 不需要定义构造函数 | 无法传递参数 |

2.5 寄生式继承

实现原理:在原型式继承的基础上,增强对象,返回增强后的对象。

// 原型式继承函数
function objectCreate(proto) {
  function F() {};
  F.prototype = proto;
  return new F();
}

// 寄生式继承函数
function createDog(original) {
  // 创建对象
  const clone = objectCreate(original);
  // 增强对象
  clone.bark = function() {
    console.log('Woof! Woof!');
  };
  // 返回对象
  return clone;
}

// 使用示例
const animal = {
  name: '动物',
  eat: function() {
    console.log(`${this.name} is eating`);
  }
};

const dog = createDog(animal);
dog.name = '狗';
console.log(dog.name); // 狗
dog.eat(); // 狗 is eating
dog.bark(); // Woof! Woof!

优缺点: | 优点 | 缺点 | |------|------| | 可以增强对象,添加新的方法 | 与原型式继承一样,引用类型属性会被共享 | | 不需要定义构造函数 | 每次创建对象都会创建新的方法,造成内存浪费 |

2.6 寄生组合式继承

实现原理:结合寄生式继承和组合继承的优点,通过寄生式继承父类的原型,然后将结果指定给子类的原型。

// 寄生组合式继承的核心函数
function inheritPrototype(subType, superType) {
  // 创建父类原型的副本
  const prototype = Object.create(superType.prototype);
  // 修复constructor指向
  prototype.constructor = subType;
  // 将子类原型指向这个副本
  subType.prototype = prototype;
}

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 子类
function Dog(name, age) {
  Animal.call(this, name);
  this.age = age;
}

// 关键:继承父类原型
inheritPrototype(Dog, Animal);

// 添加子类特有方法
Dog.prototype.bark = function() {
  console.log('Woof! Woof!');
};

// 测试
const dog = new Dog('小黑', 2);
console.log(dog.name); // 小黑
dog.eat(); // 小黑 is eating
dog.bark(); // Woof! Woof!
console.log(Dog.prototype.constructor === Dog); // true

优缺点: | 优点 | 缺点 | |------|------| | 父类构造函数只调用一次 | 实现较为复杂 | | 避免了原型链上的多余属性 | | | 是目前最理想的继承方式 | |

三、ES6 Class继承

ES6引入了class关键字,提供了更简洁的语法来实现继承。虽然class本质上是原型继承的语法糖,但它使代码更加清晰易懂。

3.1 Class基本语法

// 父类
class Animal {
  // 构造函数
  constructor(name) {
    this.name = name;
    this.colors = ['black', 'white'];
  }

  // 原型方法
  eat() {
    console.log(`${this.name} is eating`);
  }

  // 静态方法(不会被实例继承,只能通过类调用)
  static isAnimal(obj) {
    return obj instanceof Animal;
  }
}

// 子类继承父类
class Dog extends Animal {
  // 构造函数
  constructor(name, age) {
    // 调用父类构造函数
    super(name);
    this.age = age;
  }

  // 子类特有方法
  bark() {
    console.log('Woof! Woof!');
  }

  // 重写父类方法
  eat() {
    console.log(`${this.name} is eating quickly`);
  }
}

// 使用示例
const dog = new Dog('小黑', 2);
console.log(dog.name); // 小黑
dog.eat(); // 小黑 is eating quickly
dog.bark(); // Woof! Woof!
console.log(Animal.isAnimal(dog)); // true

3.2 extends关键字的工作原理

extends关键字不仅可以继承类,还可以继承原生构造函数(如Array、Object等)。

// 继承Array
class MyArray extends Array {
  // 自定义方法
  first() {
    return this[0];
  }

  last() {
    return this[this.length - 1];
  }
}

const arr = new MyArray(1, 2, 3, 4);
console.log(arr.first()); // 1
console.log(arr.last()); // 4
console.log(arr instanceof Array); // true

3.3 super关键字

super关键字有两种用法:

  1. 作为函数调用:super(...args),代表父类的构造函数
  2. 作为对象调用:super.method(),代表父类的原型对象
class Animal {
  constructor(name) {
    this.name = name;
  }

  eat() {
    console.log(`${this.name} is eating`);
  }
}

class Dog extends Animal {
  constructor(name, age) {
    // 调用父类构造函数
    super(name);
    this.age = age;
  }

  eat() {
    // 调用父类的eat方法
    super.eat();
    console.log('after eating');
  }
}

const dog = new Dog('小黑', 2);
dog.eat();
// 输出:
// 小黑 is eating
// after eating

3.4 Class继承与ES5继承的区别

ES5继承ES6 Class继承
通过原型链和构造函数实现通过extends和super关键字实现
子类构造函数需要手动调用父类构造函数子类构造函数必须调用super(),否则会报错
无法继承原生构造函数可以继承原生构造函数
原型方法需要手动添加到原型对象上方法直接定义在class内部,自动成为原型方法
静态方法需要手动添加到构造函数上使用static关键字定义静态方法

四、原型链与继承的应用场景

4.1 实现对象的复用

通过原型继承可以实现对象方法的复用,避免每个实例都创建相同的方法。

// 公共方法定义在原型上
const utils = {
  formatDate(date) {
    return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
  },

  formatPrice(price) {
    return `¥${price.toFixed(2)}`;
  }
};

// 其他对象继承utils
const orderUtils = Object.create(utils);
orderUtils.calculateTotal = function(items) {
  return items.reduce((total, item) => total + item.price, 0);
};

console.log(orderUtils.formatPrice(100)); // ¥100.00

4.2 React组件继承

在React中,可以通过继承React.Component来创建组件。

import React from 'react';

// 父组件
class BaseComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false,
      error: null
    };
  }

  // 公共方法
  showLoading() {
    this.setState({ loading: true });
  }

  hideLoading() {
    this.setState({ loading: false });
  }
}

// 子组件继承父组件
class UserComponent extends BaseComponent {
  constructor(props) {
    super(props);
    this.state = {
      ...this.state,
      users: []
    };
  }

  componentDidMount() {
    this.showLoading();
    // 加载数据
    fetch('/api/users')
      .then(res => res.json())
      .then(users => {
        this.setState({ users });
        this.hideLoading();
      })
      .catch(error => {
        this.setState({ error });
        this.hideLoading();
      });
  }

  render() {
    if (this.state.loading) return <div>Loading...</div>;
    if (this.state.error) return <div>Error: {this.state.error.message}</div>;

    return (
      <ul>
        {this.state.users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    );
  }
}

4.3 Vue组件继承

在Vue中,可以通过extends选项来继承组件。

// 父组件
const BaseComponent = {
  data() {
    return {
      loading: false,
      error: null
    };
  },
  methods: {
    showLoading() {
      this.loading = true;
    },
    hideLoading() {
      this.loading = false;
    }
  }
};

// 子组件继承父组件
export default {
  extends: BaseComponent,
  data() {
    return {
      users: []
    };
  },
  mounted() {
    this.showLoading();
    // 加载数据
    fetch('/api/users')
      .then(res => res.json())
      .then(users => {
        this.users = users;
        this.hideLoading();
      })
      .catch(error => {
        this.error = error;
        this.hideLoading();
      });
  }
};

五、原型链常见问题与解决方案

5.1 原型污染

原型污染(Prototype Pollution) 是指修改了Object.prototype等内置对象的原型,可能导致意想不到的后果。

// 原型污染示例
Object.prototype.log = function() {
  console.log(this);
};

const obj = {};
obj.log(); // 会调用原型上的log方法

// 更危险的情况
const user = { name: '张三' };
// 如果恶意代码修改了原型
Object.prototype.toString = function() {
  return '被污染了';
};
console.log(user.toString()); // 被污染了

防御措施

  1. 避免直接修改内置对象的原型
  2. 使用Object.create(null)创建没有原型的对象
  3. 使用hasOwnProperty方法检查属性是否是对象自身的
// 创建没有原型的对象
const safeObj = Object.create(null);
// 这样就不会继承Object.prototype上的属性
console.log(safeObj.toString); // undefined

// 使用hasOwnProperty检查属性
const obj = { a: 1 };
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key); // a
  }
}

5.2 原型链过长

过长的原型链会影响性能,查找属性时需要遍历更多的对象。

解决方案

  1. 避免过深的继承层次
  2. 优先使用组合而非继承
  3. 使用Object.hasOwn()(ES2022)快速检查属性
// 组合优于继承
const canEat = {
  eat() {
    console.log(`${this.name} is eating`);
  }
};

const canBark = {
  bark() {
    console.log('Woof! Woof!');
  }
};

// 通过Object.assign组合功能
const dog = Object.assign({
  name: '狗'
}, canEat, canBark);

dog.eat(); // 狗 is eating
dog.bark(); // Woof! Woof!

5.3 忘记调用super()

在ES6 Class继承中,如果子类定义了constructor,必须调用super(),否则会报错。

class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Dog extends Animal {
  constructor(name, age) {
    // 忘记调用super()
    this.age = age; // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  }
}

解决方案:确保在子类构造函数中首先调用super()

class Dog extends Animal {
  constructor(name, age) {
    super(name); // 必须先调用super()
    this.age = age;
  }
}

六、继承方式对比与选择指南

6.1 各种继承方式对比表

继承方式实例属性继承原型方法继承引用类型共享父类构造函数调用次数复杂度推荐指数
原型链继承1简单★★☆☆☆
构造函数继承1简单★★☆☆☆
组合继承2中等★★★☆☆
原型式继承0简单★★☆☆☆
寄生式继承0中等★★☆☆☆
寄生组合式继承1复杂★★★★☆
ES6 Class继承1简单★★★★★

6.2 继承方式选择建议

  1. ES6环境:优先使用class + extends,语法清晰,易于维护
  2. ES5环境:使用寄生组合式继承,这是ES5中最理想的继承方式
  3. 简单对象复用:使用Object.create()实现原型式继承
  4. 避免深层次继承:超过3层的继承层次会导致代码难以理解和维护
  5. 优先考虑组合:在很多情况下,组合(将功能封装到对象中,通过Object.assign组合)比继承更灵活

七、面试题解析

7.1 题目1:以下代码输出什么?为什么?

function Foo() {
  getName = function() { console.log(1); };
  return this;
}

Foo.getName = function() { console.log(2); };

Foo.prototype.getName = function() { console.log(3); };

var getName = function() { console.log(4); };

function getName() { console.log(5); }

// 输出以下结果
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

答案与解析

  1. Foo.getName():输出2,调用Foo的静态方法getName
  2. getName():输出4,变量声明提升优先级高于函数声明
  3. Foo().getName():输出1,Foo函数修改了全局的getName
  4. getName():输出1,全局的getName已经被修改
  5. new Foo.getName():输出2,相当于new (Foo.getName())
  6. new Foo().getName():输出3,先创建Foo实例,再调用实例的getName方法(原型上的方法)
  7. new new Foo().getName():输出3,相当于new (new Foo().getName())

7.2 题目2:如何实现一个完整的继承?

答案:使用寄生组合式继承或ES6 Class继承。

// 寄生组合式继承实现
function inheritPrototype(subType, superType) {
  const prototype = Object.create(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

7.3 题目3:什么是原型链?原型链的终点是什么?

答案

  • 原型链是JavaScript中实现继承的机制,每个对象都有一个原型,原型对象也可能有自己的原型,这样就形成了一条链式结构,称为原型链。
  • 当访问对象的属性时,JavaScript引擎会沿着原型链向上查找,直到找到该属性或到达原型链的终点。
  • 原型链的终点是nullObject.prototype.__proto__的值就是null

7.4 题目4:instanceof运算符的工作原理是什么?

答案instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。

实现原理:

function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left);
  // 获取构造函数的prototype
  const prototype = right.prototype;

  // 遍历原型链
  while (true) {
    if (proto === null) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

7.5 题目5:Object.prototype.toString.call()为什么能判断数据类型?

答案: 因为每个对象的toString方法都继承自Object.prototype,但很多内置对象(如Array、Date等)重写了toString方法。Object.prototype.toString方法返回一个表示对象类型的字符串,格式为[object Type],其中Type是对象的类型。

console.log(Object.prototype.toString.call(123)); // [object Number]
console.log(Object.prototype.toString.call('abc')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]

八、总结与展望

8.1 核心知识点回顾

  • 原型链是JavaScript实现继承的基础,每个对象都有一个原型,形成链式结构
  • 继承的本质是复用代码,避免重复劳动
  • ES5中有多种继承方式,各有优缺点,寄生组合式继承是ES5中最理想的继承方式
  • ES6的classextends提供了更简洁的继承语法
  • 继承虽然强大,但过度使用会导致代码耦合度高,应优先考虑组合
  • 原型污染是一个需要注意的安全问题,避免修改内置对象的原型

8.2 未来发展趋势

  • 随着ES6及以上版本的普及,class语法会成为主流
  • 函数式编程思想的兴起,组合(composition)会越来越多地替代继承
  • TypeScript等静态类型语言的普及,会提供更严格的类型检查,减少继承相关的错误
  • Web Components标准中的自定义元素也使用了类继承的思想

九、互动与反馈

如果本文对你有帮助,请点赞、收藏、关注三连支持!如果你有任何问题或建议,欢迎在评论区留言讨论。下一篇文章我们将深入探讨JavaScript的异步编程模型,敬请期待!

【免费下载链接】Project-Ideas-And-Resources A Collection of application ideas that can be used to improve your coding skills ❤. 【免费下载链接】Project-Ideas-And-Resources 项目地址: https://gitcode.com/GitHub_Trending/pr/Project-Ideas-And-Resources

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值