JavaScript原型与super关键字

一、原型方法与对象方法优先级

js 复制代码
let a = {
  show() {
    console.log("对象方法!");
  },
};
a.__proto__.show = function () {
  console.log("原型方法!");
};

a.show(); // 输出:对象方法!
/*
 执行对象方法时,
 先查看对象有无该方法,
 有的话直接使用对象方法,没有的话再沿着原型链逐步向上查找。
*/
js 复制代码
function Abc() {}
Abc.prototype.show = function () {
  console.log("protoType show");
};

let abc = new Abc();
abc.__proto__.show = function () {
  console.log("__proto__ show");
};

abc.show(); // 输出:protoType show。
/*
 function函数比较特别。
 使用new创建对象后,调用方法时,
 会先在函数的prototype对象上查找,未找到再沿着原型链去找。
*/

二、指定对象的原型

js 复制代码
Object.setPrototypeOf(obj, prototype);
/*
 obj:
 要设置其原型的对象。

 prototype:
 该对象的新原型(一个对象或null)。

 返回值:
 指定的对象。
*/

三、"in" vs "hasOwnProperty()"

js 复制代码
/*
 1、"in"检测某一属性、方法等是否存在于对象或对象的原型链上。
 2、"hasOwnProperty()"仅检测某一属性、方法等是否存在于对象上,不检测对象的原型链。
*/

let a = {
  name: "a",
  age: 18,
  sex: "male",
  hobby: ["football", "basketball"],
  info: {
    city: "beijing",
    country: "china",
  },
  getName() {
    return this.name;
  },
};
let b = {
  address: "beijing",
};

Object.setPrototypeOf(b, a);

console.log("address" in b);              // true。
console.log("name" in b);                 // true。
console.log("age" in b);                  // true。
console.log("sex" in b);                  // true。
console.log("hobby" in b);                // true。
console.log("info" in b);                 // true。
console.log("city" in b);                 // false。
console.log("country" in b);              // false。
console.log("getName" in b);              // true。

console.log(b.hasOwnProperty("address")); // true。
console.log(b.hasOwnProperty("name"));    // false。
console.log(b.hasOwnProperty("age"));     // false。
console.log(b.hasOwnProperty("sex"));     // false。
console.log(b.hasOwnProperty("hobby"));   // false。
console.log(b.hasOwnProperty("info"));    // false。
console.log(b.hasOwnProperty("city"));    // false。
console.log(b.hasOwnProperty("country")); // false。
console.log(b.hasOwnProperty("getName")); // false。

// 所以遍历对象时,尽量使用"hasOwnProperty()"进行判断。
for (let key in b) {
  if (b.hasOwnProperty(key)) {
    console.log("own property: " + key); // 仅输出:own property: address。
  }
  console.log(key);
  /*
   输出:
       address
       name
       age
       sex
       hobby
       info
       getName
  */
}

四、继承(ES5)

js 复制代码
// Shape------父类。
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父类方法。
Shape.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.info("Shape moved.");
};

// Rectangle------子类。
function Rectangle() {
  Shape.call(this); // 调用父类构造函数。
}

// 子类继承父类。方式一(推荐):
/*
 注意:
     不可以直接写"Rectangle.prototype = Shape.prototype"。
     因为这样写的话,表示"Rectangle.prototype"和"Shape.prototype"都指向同一个对象(Shape.prototype)。
     一旦"Rectangle.prototype"被修改,"Shape.prototype"也会被修改。
     比如要给"Shape.prototype"添加新的属性或方法时,也会同时添加给"Rectangle.prototype"。

     所以在ES5中构造方法实现继承时,
         1、创建一个对象"obj",将"obj"赋给"Rectangle.prototype"。
         2、将"obj"的原型引用指向"Shape.prototype",
            这样"Rectangle"创建的对象就可以通过原型引用(__proto__)查找其原型"obj"的原型,
            进而可以使用"Shape.prototype"的属性和方法,从而实现继承。
         3、添加"Rectangle.prototype"的构造器(constructor)到"obj"中,
            避免通过"Rectangle"创建的对象使用的是继承的"Shape"的原型中的构造器,
            因为将"obj"赋值给"Rectangle.prototype"后,"Rectangle"自身的构造器就没了(obj中没有Rectangle的构造器)。
            所以此时我们需要手动添加"Rectangle"的构造器到"obj"中。
     如下所示:
*/
Rectangle.prototype = Object.create(Shape.prototype, {
  /*
   如果不将"Rectangle.prototype.constructor"设置为"Rectangle",
   它将采用"Shape"(父类)的"prototype.constructor"。
   为避免这种情况,我们将"prototype.constructor"设置为"Rectangle"(子类)。
  */
  constructor: {
    value: Rectangle,
    enumerable: false,
    writable: true,
    configurable: true,
  },
});

// 子类继承父类。方式二(不推荐):
Rectangle.prototype.__proto__ = Shape.prototype;

// 子类继承父类。方式三(不推荐):
Object.setPrototypeOf(Rectangle.prototype, Shape.prototype);

const rect = new Rectangle();

console.log("rect 是 Rectangle 类的实例吗?", rect instanceof Rectangle); // true。
console.log("rect 是 Shape 类的实例吗?", rect instanceof Shape);         // true。
rect.move(1, 1); // 输出:'Shape moved.'。

上述三种方式子类继承父类的代码目的相同(让Rectangle.prototype继承自Shape.prototype),但它们在实现机制、副作用、性能和规范性上存在显著差异。

三者详细对比:

  1. Rectangle.prototype = Object.create(Shape.prototype, { constructor: ... })

    推荐方式(ES5标准做法)。

    原理:

    • 创建一个全新的对象 ,其[[Prototype]]指向Shape.prototype

    • 将这个新对象赋值给Rectangle.prototype

    • 显式定义constructor属性为Rectangle,并设为不可枚举(符合内置行为)。

    优点:

    • 语义清晰 :明确表达"Rectangle.prototype是一个以Shape.prototype为原型的新对象"。

    • 无副作用 :不修改任何现有对象,只是替换prototype引用。

    • 标准兼容:ES5+正式API,所有环境支持。

    • 可精确控制属性描述符 (如enumerable: false)。

    注意:

    • 原来的Rectangle.prototype(由构造函数自动创建的那个{ constructor: Rectangle })会被丢弃。

    • 所有原本定义在旧prototype上的方法必须重新定义在新对象上(通常在设置完继承后再添加)。

      示例:

      js 复制代码
      function Shape() {}
      Shape.prototype.move = function() { console.log('move'); };
      
      function Rectangle() {}
      
      // 设置继承。
      Rectangle.prototype = Object.create(Shape.prototype, {
        constructor: { value: Rectangle, enumerable: false }
      });
      
      // 添加自身方法。
      Rectangle.prototype.area = function() { return this.w * this.h; };
  2. Rectangle.prototype.__proto__ = Shape.prototype

    不推荐(非标准、已废弃)。

    原理:

    • 直接修改现有Rectangle.prototype对象的内部[[Prototype]]

    • 不创建新对象,而是在原对象上"打补丁"。

    缺点:

    • __proto__是非标准属性 (虽被ES6 Annex B收录,但仅用于Web兼容,不建议在生产代码中使用)。

    • 破坏封装性:动态修改对象原型链,使引擎难以优化(V8会deoptimize)。

    • 可能引发意外行为 :如果之前已在Rectangle.prototype上定义了方法,虽然保留,但语义混乱。

    • 在严格模式或某些JS引擎中可能被禁用

    • 无法干净控制constructor属性的行为(虽然它还在,但容易被忽略)。

    本质:

    • 这是对已有对象的运行时篡改,而非声明式构建。
  3. Object.setPrototypeOf(Rectangle.prototype, Shape.prototype)

    可用但不推荐(性能差)。

    原理:

    • __proto__效果完全相同:修改现有对象的原型链。

    • 但使用的是ES6标准APIObject.setPrototypeOf是正式方法)。

    优点:

    • 是标准方法,比__proto__更"合法"。

    • 语义明确:"设置某对象的原型"。

    缺点:

    • 仍然修改现有对象的原型链,而非创建新对象。

    • 性能极差 :现代JS引擎(如V8)会对动态修改[[Prototype]]的对象进行严重去优化。

    • TypeScript官方文档、MDN、Google JS Style Guide均明确建议避免使用

    • 同样存在constructor管理不清晰的问题。

三者核心区别总结:

特性 Object.create(...) prototype.__proto__ = ... Object.setPrototypeOf(...)
是否创建新对象 是。 否(修改原对象)。 否(修改原对象)。
是否标准 ES5+。 非标准(遗留)。 ES6标准。
性能 快(静态结构)。 慢(动态修改)。 慢(动态修改)。
可维护性 高(声明式)。 低(隐式修改)。 低(副作用)。
constructor控制 精确。 依赖原有。 依赖原有。
推荐度 强烈推荐。 禁止使用。 仅用于特殊场景(如polyfill)。

为什么Object.create是最佳选择?

因为它遵循了"组合优于修改"和"不可变思维":

  • 不动原始对象。

  • 创建一个干净的新原型链。

  • 结构清晰,易于理解和调试。

  • 与现代JS(包括class extends内部实现)理念一致。

实际上,class Rectangle extends Shape {}在底层就是用类似Object.create(Shape.prototype)的方式设置原型的。

总结:

写法 是否应该用? 原因
Object.create(...) 应该用 标准、高效、清晰。
prototype.__proto__ = ... 不要用 非标准、危险、过时。
Object.setPrototypeOf(...) 避免用 标准但性能差,仅限不得已场景。

记住:继承的本质是"建立原型链",而不是"修改已有对象的原型"。所以,创建新对象,胜过修改旧对象

五、Mixin实现"多继承"

js 复制代码
function Address() {}
Address.prototype.getAddress = function () {
  console.log("这是地址!");
};

function Credit() {}
Credit.prototype.total = function () {
  console.log("这是积分统计!");
};

function Request() {}
Request.prototype.ajax = function () {
  console.log("这是请求后台!");
};

function User(name, age) {
  this.name = name;
  this.age = age;
}
User.prototype.show = function () {
  console.log(this.name, this.age);
};

/*
 此时"User"实例要想实现"Address"、"Credit"、"Request"功能,
 可让"User"继承"Request","Request"继承"Credit","Credit"继承"Address"。
 但这样多继承,会造成混乱。
*/

使用Mixin(混合功能):

js 复制代码
// 第一步:将"Request"、Credit"、Address"全部变为对象。

const Address = {
  getAddress() {
    console.log("这是地址!");
  },
  setAddress() {
    console.log("这是设置地址!");
  },
};

const Credit = {
  total() {
    console.log("这是积分统计!");
  },
};

const Request = {
  ajax() {
    console.log("这是请求后台!");
  },
};

// 第二步:将上述对象中的方法赋值给"User"原型。

function User(name, age) {
  this.name = name;
  this.age = age;
}
User.prototype.show = function () {
  console.log(this.name, this.age);
};

/*
 User.prototype.getAddress = Address.getAddress;
 User.prototype.setAddress = Address.setAddress;
 User.prototype.total = Credit.total;
 User.prototype.ajax = Request.ajax
*/
// 若要赋值给"User"原型的方法过多,可使用Object.assign()方法。
User.prototype = Object.assign(User.prototype, Address, Credit, Request);

let user = new User("张三", 18);
user.getAddress();
user.setAddress();
user.total();
user.ajax();
user.show();

六、super

js 复制代码
const Base = {
  getInfo() {
    return "这是Base!";
  },
};

const Address = {
  // "__proto__: Base,"方法不推荐,建议使用如下"Object.setPrototypeOf()"方法实现对象继承。
  getInfo() {
    return "这是Address!";
  },
};
Object.setPrototypeOf(Address, Base);

const Credit = {
  total() {
    console.log(this.getInfo() + "+这是积分统计!"); // 用"super.getInfo()",原因如下。
  },
};
Object.setPrototypeOf(Credit, Address);

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

// 将Credit对象的自有属性复制到User.prototype,而不会复制Credit对象的原型链。
User.prototype = Object.assign(User.prototype, Credit);

let user = new User("张三", 18);

Credit.total(); // 正确。
/*
 上述"total()"方法中使用"this.getInfo()"或"super.getInfo()"时,
 输出都是:这是Address!+这是积分统计!

 因为"Credit"原型引用指向的"Address"对象中有"getInfo()"方法,
 所以JS不会继续往上找"Address"原型引用指向的"Base"对象中的"getInfo()"方法。
*/

user.total(); // 报错:TypeError: this.getInfo is not a function。
/*
 "Credit.total();"
 正确是因为JS在Credit对象中没找到"this.getInfo()"方法时(this指向Credit对象),
 会去Credit对象的原型链中查找。

 "user.total();"
 错误是因为JS在user对象中没找到"this.getInfo()"方法时(this指向user对象),
 会去user对象原型链中查找。
 而user对象原型链中找不到"this.getInfo()"方法,所以报错。

 解决办法:
     将"this.getInfo()"改为"super.getInfo()"。
*/
js 复制代码
const Base = {
  getInfo1() {
    return "这是Base!";
  },
};

const Address = {
  getInfo2() {
    return this.getInfo1();
  },
};
Object.setPrototypeOf(Address, Base);

const Credit = {
  total() {
    console.log(this.getInfo2() + "+这是积分统计!");
  },
};
Object.setPrototypeOf(Credit, Address);

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

Object.assign(User.prototype, Credit);

let user = new User("张三", 18);

Credit.total(); // 正确。
/*
 上述"total()"方法中"this.getInfo2()"、"getInfo2()"方法中"this.getInfo1()",
 上述使用"this"或将"this"全部改为"super"。
 输出都是:这是Base!+这是积分统计!

 因为使用"this"调用方法时,若对象中没有,则会去对象原型链中查找。
 而使用"super"时,则表示直接去对象的原型中查找。

 上述"this.getInfo2()"、"this.getInfo1()"中的this全部指向Credit对象。
*/

user.total(); // 报错:TypeError: this.getInfo2 is not a function。
/*
 上述"this.getInfo2()"、"this.getInfo1()"中的this全部指向user对象。

 解决办法:
     将"this.getInfo2()"、"this.getInfo1()"中的this全部改为"super"。
*/

/*
 1、"super"的核心规则。
     "super"在对象方法中的指向由方法的"[[HomeObject]]"属性决定:
         1、方法定义在哪个对象中,该对象就是方法的"[[HomeObject]]"。
         2、"super"始终指向"[[HomeObject]]"的原型对象。
         3、即使方法被复制到其他对象,其"[[HomeObject]]"和"super"指向也不会改变。
     "[[HomeObject]]"介绍:
         "[[HomeObject]]"是ES6引入的一个内部槽(internal slot),用于支持"super"的静态绑定。
         它的作用是:记录这个方法"属于哪个对象"。
     "super.method();":
         1、获取当前类的父类的原型(即"Object.getPrototypeOf(Child.prototype) → Parent.prototype")。
         2、在这个原型对象上查找"method"。
         3、如果没找到,就继续沿着"Parent.prototype"的"[[Prototype]]"向上找(即"GrandParent.prototype",依此类推)。
         4、这和调用"this.method()"的查找路径几乎一样,只是起点不同:
             1、this.method():从当前实例的原型(Child.prototype)开始找。
             2、super.method():从父类的原型(Parent.prototype)开始找。

 2、上述方法中"super"分析。
     "getInfo2()"方法定义在"Address"对象中,因此"[[HomeObject]] = Address"。
     "Address"的原型是"Base"(通过"Object.setPrototypeOf(Address, Base)"设置)。
     所以"super.getInfo1()"中的"super"始终指向"Base"对象

     "total()"方法定义在"Credit"对象中,因此"[[HomeObject]] = Credit"。
     "Credit"的原型是"Address"(通过"Object.setPrototypeOf(Credit, Address)"设置)。
     所以"super.getInfo2()"中的"super"始终指向"Address"对象。

 3、方法复制后的"super"指向。
     当通过"Object.assign(User.prototype, Credit)"将"Credit.total()"复制到"User.prototype"后,
     虽然"total()"方法现在存在于"User.prototype"上,但它的"[[HomeObject]]"仍然是"Credit"(定义时所在的对象)。
     因此"super.getInfo2()"中的"super"仍然指向"Address"对象,而不是"User.prototype"的原型。
     这就是为什么即使方法被复制,"super"的指向也不会改变。

 4、与"this"的本质区别。
     "this"是动态的:指向调用该方法的对象,取决于调用方式。
     "super"是静态的:指向方法定义时所在对象的原型,不会随调用方式改变。
*/

super是JavaScript中用于访问和调用父类(超类)的属性和方法 的关键字。它在类继承(class extends)场景中非常关键,尤其在子类的构造函数或方法中。

super的两种主要用法:

  1. 作为函数调用:super()

    • 只能在子类的constructor中使用

    • 作用:调用父类的构造函数,完成父类的初始化。

    • 必须在访问this之前调用(否则报错)。

      js 复制代码
      class Animal {
        constructor(name) {
          this.name = name;
        }
      }
      
      class Dog extends Animal {
        constructor(name, breed) {
          super(name);         // 调用父类构造函数,初始化this.name。
          this.breed = breed;  // 必须在super()之后才能用this。
        }
      }
      
      const dog = new Dog("Buddy", "Golden Retriever");
      console.log(dog.name);   // "Buddy"。
      console.log(dog.breed);  // "Golden Retriever"。

      错误示例:

      js 复制代码
      class Dog extends Animal {
        constructor(name) {
          this.name = name; // ReferenceError: Must call super() before accessing 'this'。
          super(name);
        }
      }
  2. 作为对象使用:super.method()super.property

    • 可以在子类的任何方法中使用(包括普通方法、静态方法)。

    • 用于调用父类的同名方法或访问父类属性。

    普通方法中使用:

    js 复制代码
    class Animal {
      speak() {
        return `makes a noise.`;
      }
    }
    
    class Dog extends Animal {
      speak() {
        // 调用父类的speak()。
        return super.speak() + " Woof!";
      }
    }
    
    const dog = new Dog();
    console.log(dog.speak()); // "makes a noise. Woof!"。

    静态方法中使用:

    js 复制代码
    class Parent {
      static hello() {
        return "Hello from Parent";
      }
    }
    
    class Child extends Parent {
      static hello() {
        return super.hello() + " and Child!";
      }
    }
    
    console.log(Child.hello()); // "Hello from Parent and Child!"
    
    /*
     静态方法不是属于类本身(类是构造函数的语法糖。构造函数也是对象,静态方法是构造函数对象的属性,不在其prototype上)吗?
     按照super的核心规则,它指向的应该是构造函数的"__proto__",即Function才对。为什么会指向父类?
     详情请参考第五章(类)第5.1节(继承(类 vs 构造函数))。
    */

super在对象字面量中的用法(ES2015+):

  • super也可以在对象的方法简写中使用(但很少见)。

    js 复制代码
    const parent = {
      greet() { return "Hi"; }
    };
    
    const child = {
      __proto__: parent,
      greet() {
        return super.greet() + " there!"; // 合法。
      }
    };
    
    console.log(child.greet()); // "Hi there!"

    注意:必须使用 方法简写语法greet() {}),不能用 greet: function() {}


在JavaScript中,super的指向需要根据方法类型上下文区分。

核心规则是:super指向方法[[HomeObject]]的原型 ,而不是方法自身的prototype(如果有的话)。

核心概念:

super的指向由方法的内部[[HomeObject]]属性决定:

  • 只有对象方法(使用简写语法)类方法类构造函数 才有[[HomeObject]]

  • [[HomeObject]]是方法定义时所在的对象(固定不变)。

  • super始终指向Object.getPrototypeOf([[HomeObject]])

不同场景下super的指向:

  1. 对象字面量方法中的super

    js 复制代码
    const Parent = { x: 1 };
    const Child = {
      method() {
        return super.x; // super指向谁?
      }
    };
    Object.setPrototypeOf(Child, Parent);
    
    Child.method(); // 输出: 1。
    • method[[HomeObject]] = Child(方法定义在Child中)。

    • super指向Object.getPrototypeOf(Child),即Parent

    • 这里Child__proto__就是Parent,所以super指向Child.__proto__

  2. 类方法中的super

    js 复制代码
    class Parent {
      parentMethod() { return "parent"; }
    }
    class Child extends Parent {
      childMethod() {
        return super.parentMethod(); // super指向谁?
      }
    }
    • childMethod[[HomeObject]] = Child.prototype(类方法默认挂载在类的prototype上)。

    • super指向Object.getPrototypeOf(Child.prototype),即Parent.prototype

    • 这里Child.prototype.__proto__ = Parent.prototype,所以super指向Child.prototype.__proto__

  3. 类构造函数中的super(特殊情况)。

    js 复制代码
    class Parent {
      constructor(name) {
        this.name = name;
      }
    }
    class Child extends Parent {
      constructor(name, age) {
        super(name); // super指向谁?
        this.age = age;
      }
    }
    • 类构造函数中的super()特殊语法,用于调用父类构造函数。

    • 此时super指向父类构造函数本身Parent构造函数),而不是原型。

    • 执行super(name)等价于Parent.call(this, name),用于初始化父类属性。

关键区分: superprototype/__proto__的关系。

概念 指向 作用
super(对象/类方法) [[HomeObject]]的原型(即Object.getPrototypeOf([[HomeObject]]))。 访问父级原型上的属性/方法。
super(类构造函数) 父类构造函数本身。 调用父类构造函数。
对象的__proto__ 构造函数的prototype 建立对象的原型链。
函数的prototype 函数的原型对象。 用于new操作时建立实例的原型链。

常见误区:

  1. 普通函数中的super

    普通函数没有[[HomeObject]],不能使用super,会直接报错。

    js 复制代码
    function foo() {
      super.x; // 语法错误:'super' keyword unexpected here。
    }
  2. 方法复制后super的指向。

    即使方法被复制到其他对象,[[HomeObject]]super指向不变。

    js 复制代码
    const Parent = { x: 1 };
    const Child = { method() { return super.x; } };
    Object.setPrototypeOf(Child, Parent);
    
    const Other = {};
    Other.method = Child.method; // 复制方法。
    
    Other.method(); // 输出: 1(super仍指向Parent,不是Other的原型)。
相关推荐
左耳咚2 小时前
Claude Code 技术全景概览
前端·ai编程
2601_953465612 小时前
m3u8live.cn深度解析:一款专为开发者打造的 M3U8 调试工具
java·前端·django·音视频·开发工具
娇娇yyyyyy2 小时前
QT编程(9): QTextEdit
前端·qt
zzb15802 小时前
RAG from Scratch-优化-routing
java·前端·网络·人工智能·后端·python·mybatis
codeshareman2 小时前
JSON.stringify 在 React Hooks 依赖项里的坑:一次复盘
javascript
进击的尘埃2 小时前
把 LLM 吐出来的组件扔进 `iframe` 跑:沙箱隔离这件事没你想的那么简单
javascript
ujainu2 小时前
Electron 极简时钟应用开发全解析:托盘驻留、精准北京时间与 HarmonyOS PC 适配实战
javascript·electron·harmonyos
清空mega2 小时前
《Vue Router 与 Pinia 入门:页面跳转、动态路由、全局状态管理一篇打通》
前端·javascript·vue.js
进击的尘埃2 小时前
从一个 `console.log` 顺序翻车说起,聊聊微任务那些糟心事
javascript