JavaScript 的面向对象 (OOP) 与传统面向对象语言 (如 Java、C++) 差异显著 —— 它没有类 (class) 的原生概念 (ES6 class 是语法糖),而是基于原型 (Prototype) 实现面向对象核心特性 (封装、继承、多态)

面向对象的核心基石

万物皆对象?不完全是

  1. 基本类型:string、number、boolean、null、undefined、symbol、bigint (非对象,无属性 / 方法)

  2. 引用类型:object (包括普通对象、数组、函数、日期、正则等)—— 核心是 「键值对集合」,且每个对象都关联一个 「原型对象」

原型(Prototype):OOP 的核心

  1. 每个对象 (除 Object.create(null) 创建的纯对象) 都有一个隐藏的 [[Prototype]] 内置属性 (可通过 __proto__ 访问,标准方式是 Object.getPrototypeOf()),指向它的原型对象;

  2. 原型对象本身也是对象,也有自己的原型,形成 「原型链」,最终指向 null (原型链的终点)

    const obj = {};
    console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
    console.log(Object.getPrototypeOf(Object.prototype)); // null(原型链终点)
    

构造函数(Constructor)

  1. 构造函数是普通函数 (首字母大写约定),通过 new 调用时:

    1. 创建一个空对象;
    2. 该对象的 [[Prototype]] 指向构造函数的 prototype 属性;
    3. 构造函数的 this 绑定到这个空对象;
    4. 执行构造函数代码 (初始化属性)
    5. 若构造函数无显式返回对象,则返回这个新对象;
  2. 示例:

    // 构造函数
    function Person(name, age) {
      this.name = name; // 实例属性
      this.age = age;
    }
    // 原型方法(所有实例共享)
    Person.prototype.sayHi = function() {
      console.log(`Hi, I'm ${this.name}`);
    };
    
    // 创建实例
    const p1 = new Person("张三", 20);
    p1.sayHi(); // Hi, I'm 张三
    console.log(p1.__proto__ === Person.prototype); // true
    

面向对象的三大特性

封装:隐藏内部细节,暴露公共接口

  1. 封装的核心是「控制属性 / 方法的访问权限」,但 JS 没有原生的 private/protected 修饰符 (ES6+ 提供了模拟方案)

  2. 实现:

    function Counter() {
      // 实例私有属性(外部无法直接访问)
      let count = 0; 
    
      // 公共方法(暴露接口)
      this.increment = function() {
        count++;
        return count;
      };
    
      this.getCount = function() {
        return count;
      };
    }
    
    const c = new Counter();
    c.increment(); // 1
    console.log(c.count); // undefined(私有属性不可访问)
    
    class Counter {
      #count = 0; // 私有实例字段
    
      increment() {
        this.#count++;
        return this.#count;
      }
    
      #log() { // 私有方法
        console.log("Count changed");
      }
    
      getCount() {
        this.#log();
        return this.#count;
      }
    }
    
    const c = new Counter();
    c.increment(); // 1
    c.getCount(); // 打印 "Count changed",返回 1
    console.log(c.#count); // 报错:私有字段不可访问
    
    function createPerson(name) {
      // 私有变量
      let age = 0;
      return {
        getName: () => name,
        getAge: () => age,
        setAge: (newAge) => {
          if (newAge >= 0) age = newAge;
        }
      };
    }
    
    const p = createPerson("李四");
    p.setAge(25);
    console.log(p.getName(), p.getAge()); // 李四 25
    console.log(p.age); // undefined
    

继承:复用已有对象的属性 / 方法

JS 继承的核心是「原型链继承」,ES6 之前需手动拼接原型链,ES6 class 简化了语法 (但底层仍是原型)

组合继承(原型链 + 构造函数继承)

  1. 核心逻辑:

    1. 构造函数.call(this) 继承父类实例属性 (解决原型链继承的属性共享问题)
    2. 子类.prototype = new 父类() 继承父类原型方法;
    3. 修复子类构造函数指向 (避免 constructor 错乱)
  2. 示例代码:

    // 父类
    function Animal(name) {
      this.name = name;
      this.eat = () => console.log(`${name} 吃食物`);
    }
    // 父类原型方法
    Animal.prototype.run = function() {
      console.log(`${this.name} 跑`);
    };
    
    // 子类
    function Dog(name, breed) {
      // 1. 构造函数继承(继承父类实例属性,避免引用类型共享)
      Animal.call(this, name); 
      this.breed = breed; // 子类自有属性
    }
    
    // 2. 核心:让子类原型指向父类实例(建立原型链)
    Dog.prototype = new Animal();
    // 修复构造函数指向(否则 Dog 实例的 constructor 是 Animal)
    Dog.prototype.constructor = Dog;
    
    // 子类原型方法
    Dog.prototype.bark = function() {
      console.log(`${this.name} 汪汪叫`);
    };
    
    // 使用
    const dog = new Dog("旺财", "中华田园犬");
    dog.eat(); // 旺财 吃食物
    dog.run(); // 旺财 跑
    dog.bark(); // 旺财 汪汪叫
    console.log(dog.breed); // 中华田园犬
    
  3. 图解:

寄生组合继承(优化组合继承)

  1. 核心优化点:

    1. 组合继承会调用 2 次父类构造函数 (Animal.call + new Animal),导致子类原型冗余;
    2. 寄生组合继承通过 Object.create(父类.prototype) 直接继承父类原型,仅调用 1 次父类构造函数;
  2. 示例代码:

    // 父类:动物(同组合继承)
    function Animal(name) {
      this.name = name;
      this.skills = ["跑", "吃"];
      console.log("Animal 构造函数被调用"); // 用于验证调用次数
    }
    
    Animal.prototype.eat = function() {
      console.log(`${this.name} 正在吃食物`);
    };
    
    // 子类:猫
    function Cat(name, color) {
      // 仅这 1 次调用父类构造函数(核心优化)
      Animal.call(this, name); 
      this.color = color;
    }
    
    // 寄生继承核心:创建空对象,原型指向父类原型(避免调用父类构造函数)
    Cat.prototype = Object.create(Animal.prototype);
    // 修复构造函数指向
    Cat.prototype.constructor = Cat;
    
    // 子类原型方法
    Cat.prototype.meow = function() {
      console.log(`${this.name}(${this.color})喵喵叫`);
    };
    
    // 测试示例
    const cat1 = new Cat("小白", "白色");
    const cat2 = new Cat("小黑", "黑色");
    
    // 1. 验证父类构造函数仅调用 1 次(控制台只打印 1 次 "Animal 构造函数被调用")
    // 2. 验证实例属性不共享
    cat1.skills.push("抓老鼠");
    console.log(cat1.skills); // ["跑", "吃", "抓老鼠"]
    console.log(cat2.skills); // ["跑", "吃"]
    
    // 3. 验证继承父类方法
    cat2.eat(); // 小黑 正在吃食物
    
    // 4. 验证子类自有方法
    cat1.meow(); // 小白(白色)喵喵叫
    
    // 5. 验证原型链干净(无冗余的父类实例属性)
    console.log(Cat.prototype.name); // undefined(组合继承会是 undefined 吗?组合继承中是 undefined,因为 new Animal() 没传参,但调用了构造函数)
    // 对比:组合继承中 Dog.prototype.name 是 undefined(因为 new Animal() 无参),但寄生组合继承更彻底
    
  3. 图解

ES6 class 继承(语法糖)

  1. class + extends + super 简化继承逻辑,底层仍是原型链;

  2. 示例代码:

    class Animal {
      constructor(name) {
        this.name = name;
      }
    
      eat() {
        console.log(`${this.name} 吃食物`);
      }
    
      run() {
        console.log(`${this.name} 跑`);
      }
    }
    
    // 子类继承父类
    class Dog extends Animal {
      constructor(name, breed) {
        super(name); // 调用父类构造函数(必须在 this 前)
        this.breed = breed;
      }
    
      bark() {
        console.log(`${this.name} 汪汪叫`);
      }
    
      // 重写父类方法(多态)
      run() {
        super.run(); // 调用父类 run 方法
        console.log(`${this.name} 飞快地跑`);
      }
    }
    
    const dog = new Dog("来福", "金毛");
    dog.eat(); // 来福 吃食物
    dog.run(); // 来福 跑 → 来福 飞快地跑
    dog.bark(); // 来福 汪汪叫
    

多态:同一方法,不同对象表现不同行为

  1. JS 是弱类型语言,多态无需像强类型语言那样显式定义,核心是 「方法重写」「动态绑定」

  2. 示例代码:

    class Shape {
      area() {
        return 0; // 基类默认实现
      }
    }
    
    class Circle extends Shape {
      constructor(radius) {
        super();
        this.radius = radius;
      }
    
      area() {
        return Math.PI * this.radius **2; // 重写面积计算
      }
    }
    
    class Rectangle extends Shape {
      constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
      }
    
      area() {
        return this.width * this.height; // 重写面积计算
      }
    }
    
    // 多态体现:统一接口,不同实现,子类重写父类的方法,调用时根据实例类型执行对应逻辑
    function calculateArea(shape) {
      console.log(shape.area());
    }
    
    calculateArea(new Circle(5)); // 78.5398...
    calculateArea(new Rectangle(4, 6)); // 24
    calculateArea(new Shape()); // 0
    
    // 无需继承关系,只要有 fly 方法即可,体现 “鸭子类型”(如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子)
    const Bird = { fly: () => console.log("鸟飞") };
    const Plane = { fly: () => console.log("飞机飞") };
    
    function letItFly(obj) {
      obj.fly(); // 多态:不同对象执行不同 fly 逻辑
    }
    
    letItFly(Bird); // 鸟飞
    letItFly(Plane); // 飞机飞
    

ES6 class 深度解析(语法糖的本质)

  1. ES6 引入的 class 是原型继承的语法糖,没有改变 JS 基于原型的本质,但让代码更接近传统 OOP 风格;

  2. 以下两段代码实现了同样的继承关系:

    class Person {
      // 静态属性(ES7+)
      static species = "Human";
      // 实例属性(ES7+,也可写在 constructor 里)
      gender = "unknown";
    
      // 构造函数
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    
      // 原型方法(实例方法)
      sayHi() {
        console.log(`Hi, ${this.name}`);
      }
    
      // 静态方法(通过类调用,无 this)
      static createAdult(name) {
        return new Person(name, 18);
      }
    
      // 访问器属性(get/set)
      get fullInfo() {
        return `${this.name}, ${this.age}岁`;
      }
    
      set fullInfo(info) {
        const [name, age] = info.split(",");
        this.name = name;
        this.age = Number(age);
      }
    }
    
    // 使用
    const p = Person.createAdult("王五"); // 静态方法创建实例
    p.sayHi(); // Hi, 王五
    console.log(p.fullInfo); // 王五, 18岁
    p.fullInfo = "赵六, 22";
    console.log(p.age); // 22
    console.log(Person.species); // Human
    
    function Person(name, age) {
      this.gender = "unknown";
      this.name = name;
      this.age = age;
    }
    
    Person.species = "Human";
    Person.prototype.sayHi = function() {
      console.log(`Hi, ${this.name}`);
    };
    Person.createAdult = function(name) {
      return new Person(name, 18);
    };
    // 访问器属性
    Object.defineProperty(Person.prototype, "fullInfo", {
      get() {
        return `${this.name}, ${this.age}岁`;
      },
      set(info) {
        const [name, age] = info.split(",");
        this.name = name;
        this.age = Number(age);
      }
    });
    

常见误区与注意事项

混淆 __proto__ 和 prototype

  1. prototype:构造函数的属性,指向原型对象 (供实例继承)

  2. __proto__:实例的属性,指向构造函数的 prototype (即 Object.getPrototypeOf(实例))

    function Foo() {}
    const foo = new Foo();
    
    console.log(Foo.prototype); // Foo 的原型对象
    console.log(foo.__proto__ === Foo.prototype); // true
    console.log(Foo.__proto__ === Function.prototype); // true(函数也是对象)
    

原型方法 vs 实例方法

  1. 原型方法:定义在 构造函数.prototype 上,所有实例共享 (节省内存)

  2. 实例方法:定义在构造函数内部 (this.方法),每个实例独立拥有 (占用更多内存)

    function A() {
      this.fn1 = () => {}; // 实例方法(每个实例一个副本)
    }
    A.prototype.fn2 = () => {}; // 原型方法(所有实例共享)
    
    const a1 = new A();
    const a2 = new A();
    console.log(a1.fn1 === a2.fn1); // false
    console.log(a1.fn2 === a2.fn2); // true
    

避免修改原生对象的原型

  1. 如修改 Array.prototypeObject.prototype,可能导致全局污染和兼容性问题;

  2. 示例代码:

    // 不推荐!
    Array.prototype.add = function(val) {
      this.push(val);
    };
    // 可能与其他库冲突,或影响 for...in 遍历
    

面试题

手写简易版 Object.create

  1. 核心目标:创建一个新对象,让其原型指向传入的对象 (忽略 null 原型、第二个参数等细节)

  2. 核心逻辑:通过空构造函数关联原型,再实例化得到新对象;

function Animal(kind) {
  this.kind = kind;
}
function Dog(name) {
  this.name = name;
}

function _create(proto) {
  // 1. 定义空构造函数
  function F() {}
  // 2. 让构造函数的原型指向传入的 proto
  F.prototype = proto;
  // 3. 实例化构造函数,返回新对象(新对象 __proto__ 指向 proto)
  return new F();
}

// 测试
Dog.prototype = _create(Animal);
let dog = new Dog('花花');
console.log(dog.__proto__.__proto__ === Animal.prototype); // true
console.log(dog.__proto__ === Dog.prototype); // true

手写简易版 new

  1. 核心目标:模拟 new 的核心流程 (创建对象→绑定原型→执行构造函数→返回对象),忽略边界校验、显式返回对象等细节;

  2. 核心逻辑:创建空对象 绑定原型 改变构造函数 this 指向 执行构造函数 返回新对象;

/***
 * @param {*} Func 操作的那个类
* @param  {...any} args new 的时候传入的实参集合
* @return 实例 或者 自己返回的对象
*/
function _new(Func, ...args) {
  // 1. 创建空对象,让其 __proto__ 指向构造函数的 prototype

  // let obj = {};
  // obj.__proto__ == Func.prototype;//=> IE大部分浏览器不支持直接操作 __proto__
  // 等价于
  let objInstance = Object.create(Func.prototype);

  // 2. 执行构造函数,将 this 绑定到新对象
  let result = Func.call(objInstance, ...args);

  // 3. 若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
  if ((result !== null && typeof result === 'object') || typeof result === 'function') {
    return result;
  }
  
  // 4. 返回创建的新对象
  return objInstance;
}

function Dog(name) {
  this.name = name;
}
Dog.prototype.bark = function () {
  console.log('wangwang');
}
Dog.prototype.sayName = function () {
  console.log('my name is ' + this.name);
}

let sanmao = _new(Dog, '三毛');
sanmao.sayName();
sanmao.bark();
console.log(sanmao instanceof Dog);

实现 n.plus(10).minus(5)

~ function anonymous(proto) {
  const checkNum = function checkNum(num) {
    // isNaN 是否是 非有效数字
    return isNaN(Number(num)) ? 0 : num;
  };

  proto.plus = function plus(num) {
    // this:要操作的那个数字实例(对象)
    // 返回 Number 类的实例,实现链式写法
    return this + checkNum(num);
  };

  proto.minus = function minus(num) {
    return this - checkNum(num);
  };
}(Number.prototype);

let n = 10;
console.log(n.plus(10).minus(5)); //=>15(10+10-5)

画图计算下面的结果

function fun() {
  this.a = 0;
  this.b = function () {
    alert(this.a);
  }
}

fun.prototype = {
  b: function () {
    this.a = 20;
    alert(this.a);
  },
  c: function () {
    this.a = 30;
    alert(this.a);
  }
}

var my_fun = new fun();
my_fun.b();
my_fun.c();

写出下面代码执行输出的结果

function C1(name) {
  // name:undefined 没有给实例设置私有的属性 name
  if (name) {
      this.name = name;
  }
}

function C2(name) {
  this.name = name;
  // 给实例设置私有属性 name => this.name=undefined
}

function C3(name) {
  this.name = name || 'join';
  // 给实例设置私有属性 name =>this.name=undefined || 'join'
}

C1.prototype.name = 'Tom';
C2.prototype.name = 'Tom';
C3.prototype.name = 'Tom';
alert((new C1().name) + (new C2().name) + (new C3().name));
//=> (new C1().name) 找原型上的 'Tom'
//=> (new C2().name) 找私有属性 undefined
//=> (new C3().name) 找私有属性 'join'

写出下面代码执行输出的结果(阿里)

function Foo() {
  //没有 this.XXX() 就不是私有方法
  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();// 2
getName();// 4
Foo().getName();// 1
getName();// 1

// new无参列表 优先级为18
// new有参列表 优先级为19
// 成员访问 优先级为19
// 先Foo.getName(),在new
new Foo.getName();// 2

// 先new,在调用.getName()
// Foo中没有this.getName,所以根据原型链向上查找
new Foo().getName();// 3

// 先 new Foo(),再.getName(),再 new 实例.getName,new 原型上的getName
new new Foo().getName();// 3

下面代码输出结果是什么?为什么?

// 题目
let obj = {
  2: 3,
  3: 4,
  length: 2,
  push: Array.prototype.push
}
obj.push(1);
obj.push(2);
console.log(obj);
// 解析
// 扩展:模拟 array.push 的实现
Array.prototype.push = function push(num) {
  // this:arr
  this.length = this.length || 0;
  // 拿原有 length 作为新增项的索引
  this[this.length] = num;
  // length 的长度会自动跟着累加 1
  this.length++
};
let arr = [10, 20]; //=>{ 0: 10, 1: 20, length: 2 }
arr.push(30);

let obj = {
  2: 3,
  3: 4,
  length: 2,
  push: Array.prototype.push
};
obj.push(1); // obj[2]=1  obj.length=3
obj.push(2); // obj[3]=2  obj.length=4
console.log(obj); // {2: 1, 3: 2, length: 4 ...}

let obj = {
  1: 10,
  push: Array.prototype.push
};
// this.length 默认值为 0
obj.push(20); // obj[obj.length]=20   obj[0]=20
console.log(obj); // {0:20,1:10,length:1...}
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 面向对象的核心基石
    1. 1.1. 万物皆对象?不完全是
    2. 1.2. 原型(Prototype):OOP 的核心
    3. 1.3. 构造函数(Constructor)
  2. 2. 面向对象的三大特性
    1. 2.1. 封装:隐藏内部细节,暴露公共接口
    2. 2.2. 继承:复用已有对象的属性 / 方法
      1. 2.2.1. 组合继承(原型链 + 构造函数继承)
      2. 2.2.2. 寄生组合继承(优化组合继承)
      3. 2.2.3. ES6 class 继承(语法糖)
    3. 2.3. 多态:同一方法,不同对象表现不同行为
  3. 3. ES6 class 深度解析(语法糖的本质)
  4. 4. 常见误区与注意事项
    1. 4.1. 混淆 __proto__ 和 prototype
    2. 4.2. 原型方法 vs 实例方法
    3. 4.3. 避免修改原生对象的原型
  5. 5. 面试题
    1. 5.1. 手写简易版 Object.create
    2. 5.2. 手写简易版 new
    3. 5.3. 实现 n.plus(10).minus(5)
    4. 5.4. 画图计算下面的结果
    5. 5.5. 写出下面代码执行输出的结果
    6. 5.6. 写出下面代码执行输出的结果(阿里)
    7. 5.7. 下面代码输出结果是什么?为什么?