从原型继承到类继承详解

    科技2026-08-30  3

    从原型继承到类继承详解

    构造函数的产生new 命令的原理this的产生注意点1注意点2 Function.prototype.bind()原型链构造函数的继承类取值函数(getter)和存值函数(setter)类继承判断一个类是否继承了另一个类super 关键字子类的`__proto__` 和子类的`prototype`实例的`__proto__`拓展

    构造函数的产生

    对象是一个容器,封装了属性(property)和方法(method)

    生成对象,需要一个模板.

    JavaScript 语言使用构造函数(constructor)作为对象的模板。

    所谓”构造函数”,就是专门用来生成实例对象的函数。

    new 命令的原理

    创建一个空对象,作为将要返回的对象实例。将这个空对象的原型,指向构造函数的prototype属性。将这个空对象赋值给函数内部的this关键字。开始执行构造函数内部的代码。 function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) { // 将 arguments 对象转为数组 var args = [].slice.call(arguments); // 取出构造函数 var constructor = args.shift(); // 创建一个空对象,继承构造函数的 prototype 属性 var context = Object.create(constructor.prototype); // 执行构造函数 var result = constructor.apply(context, args); // 如果返回结果是对象,就直接返回,否则返回 context 对象 return (typeof result === 'object' && result != null) ? result : context; } // 实例 var actor = _new(Person, '张三', 28);

    ⚠️如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

    this的产生

    var obj = { foo: 5 }; { foo: { [[value]]: 5 [[writable]]: true [[enumerable]]: true [[configurable]]: true } }

    JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

    原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。

    var obj = { foo: function () {} }; { foo: { [[value]]: 函数的地址 ... } } 引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。 var f = function () { console.log(this.x); } var x = 1; var obj = { f: f, x: 2, }; // 单独执行 f() // 1 // obj 环境执行 obj.f() // 2 由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了。

    注意点1

    var obj ={ foo: function () { console.log(this); } }; // 情况一 (obj.foo = obj.foo)() // window // 情况二 (false || obj.foo)() // window // 情况三 (1, obj.foo)() // window

    ⚠️上面代码中,obj.foo就是一个值。JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。

    注意点2

    var a = { p: 'Hello', b: { m: function() { console.log(this.p); } } }; a.b.m() // undefined

    ⚠️如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层。

    Function.prototype.bind()

    ⚠️bind()方法每运行一次,就返回一个新函数。

    //错误的写法 element.addEventListener('click', o.m.bind(o)); // ... element.removeEventListener('click', o.m.bind(o)); // 正确的写法 var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener); //bind()可以结合call()方法使用 [1, 2, 3].slice(0, 1) // [1] // 等同于 :本质是在[1, 2, 3]上面调用Array.prototype.slice()方法 Array.prototype.slice.call([1, 2, 3], 0, 1) // [1] //等同于: call()实质上是调用Function.prototype.call()方法 var slice = Function.prototype.call.bind(Array.prototype.slice); slice([1, 2, 3], 0, 1) // [1]

    原型链

    JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……

    修改原型对象时,一般要同时修改constructor属性的指向。

    构造函数的继承

    // 继承 // 父类 function Super() { this.x = 1; this.y = 2; this.z = 100; this.xxx = function () { console.log('父类实例享方法'); } this.arr = [1,2,3]; } Super.prototype.x = 8; Super.prototype.fff = function () { console.log('父类共享方法'); }; Super.hhh = function () { console.log('父类静态方法'); }; // 子类 function Sub() { Super.call(this); // 子类只具有父类实例的属性和方法 ,这样导致 Sub实例为Sub { x: 3, y: 4, z: 100, xxx: [Function] } this.x = 3; this.y = 4; } Sub.prototype = Object.create(Super.prototype); // 子类只具有父类原型的属性和方法 // Sub.prototype = new Super(); // 子类会具有父类原型的属性和方法 也会具有父类实例的属性和方法,但是 Sub实例依旧为Sub { x: 3, y: 4 } // Sub.prototype.__proto__ = Super.prototype; 等价于 Sub.prototype = Object.create(Super.prototype); 和 Sub.prototype.constructor = Sub; Sub.prototype.constructor = Sub; Sub.prototype.sss = function () { console.log('子类共享方法'); } // 静态属性方法继承 Sub.__proto__ = Super; // Object.create(Super.prototype) 返回一个实例,实例的原型是Super.prototype,也就是说实例继承自Super.prototype。 //Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。 const sub1 = new Sub(); console.log(sub1); //Sub { x: 3, y: 4 } sub1.fff(); console.log(sub1.x); // 3 console.log(sub1.z); // undefined | 100 // sub1.xxx(); //sub1.xxx is not a function | 父类实例享方法 const sub2 = new Sub(); sub2.arr.push(4); console.log(sub1.arr); // [ 1, 2, 3 ] | [ 1, 2, 3, 4 ] Sub.hhh(); console.log(Sub.prototype.constructor); /** * 问题1:继承父类是要部分继承(只原型)还是整体继承? 觉得Super.call(this);和 Sub.prototype = Object.create(Super.prototype); 搭配的最好 * 问题2:先创造子类实例的this,再将父类的属性方法添加到this上(Super.call(this)) * 问题3:模拟了静态属性和方法的继承 */

    类的数据类型就是函数

    类的所有方法都定义在类的prototype属性上

    取值函数(getter)和存值函数(setter)

    ⚠️目的:拦截该属性的存取行为

    class MyClass { constructor() { // ... } get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); } } let inst = new MyClass(); inst.prop = 123; // setter: 123 inst.prop // 'getter'

    类继承

    子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

    ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

    在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

    // 父类 class Super { constructor(){ this.x = 1; this.y = 2; this.z = 100; this.xxx = function () { console.log('父类实例方法'); }; this.arr = [1,2,3]; } fff() { console.log(this); // 原型方法中的this 为 子类实例 console.log('父类共享方法'); }; static hhh() { console.log(this); // 静态方法中的this 为 子类 console.log('父类静态方法'); }; } /** Sub.prototype = { constructor: Sub toString() {}, }; */ // 子类 class Sub extends Super{ // 类似ES5 的构造函数Sub // 类似ES5 constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。 constructor(x, y) { super(); // 继承 父类的 实例属性和方法 ; 即 Super.prototype.constructor.call(this)。 // 实例属性 this.x = x; this.y = y; } // 实例属性 新写法 // 那么constructor方法还有什么用?1.调用super() 2.传入参数x,y ;新写法则无法接受 bar = 'hello'; // ES5 原型上的方法 sss() { console.log(this); // 原型方法中的this 为 实例 super.fff(); // 在子类普通方法中通过`super`调用父类的方法时,方法内部的`this`指向当前的子类实例。 console.log('子类共享方法'); } // 静态方法 static staticFun() { console.log(this === Sub); // 静态方法中的this 为 类 super.hhh(); // return 'hello'; } // 静态属性 static myStaticProp = 42; // 私有属性 #count = 0; // 私有方法 #sum() { return this.#count; } } const sub1 = new Sub(3,4); console.log(sub1); //{x: 3, y: 4, z: 100, #sum: ƒ, #count: 0,arr: (3) [1, 2, 3],bar: "hello"} console.log(sub1.z); sub1.xxx(); sub1.fff(); sub1.sss(); console.log(Sub.staticFun()); console.log(Sub.myStaticProp); Sub.hhh(); // 父类的静态方法,可以被子类继承。 const sub2 = new Sub(33,44); sub2.arr.push(55); console.log(sub1.arr); // [1,2,3] // 综上, 子类 继承了 父类的实例属性和方法 ,继承了 父类的共享属性和方法 ,还继承了 静态属性和方法 // 先将父类实例对象的属性和方法,加到`this`上面(所以必须先调用`super`方法),然后再用子类的构造函数修改`this`

    判断一个类是否继承了另一个类

    Object.getPrototypeOf(ColorPoint) === Point

    super 关键字

    super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。

    super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

    由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。 即ES5的Sub.prototype = Object.create(Super.prototype);

    在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。实际上执行的是super.print.call(this)。

    在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

    在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

    子类的__proto__ 和子类的prototype

    (1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

    (2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

    class A { } class B { } // B 的实例继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype); // B 继承 A 的静态属性 (比ES5多的,也是(1)的解释) Object.setPrototypeOf(B, A); const b = new B();

    ⚠️这句话有意思:

    子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

    【子类(`B`)的原型对象(`prototype`属性)】是【父类的原型对象(`prototype`属性)的实例】。 B.prototype = Object.create(A.prototype); B.prototype.constructor = B // 等同于 子类(`B`)的原型对象(`prototype`属性)是 【父类的原型对象(`prototype`属性)】的实例。 B.prototype.__proto__ = A.prototype;

    实例的__proto__

    b.__proto__= B.prototype B.prototype.__proto__ = A.prototype;

    拓展

    Object.setPrototypeOf(A,B)方法为参数对象A设置原型B,返回该参数对象A Object.setPrototypeOf = function (obj, proto) { obj.__proto__ = proto; return obj; } 判断一个类是否继承了另外一个类 Object.getPrototypeOf(ColorPoint) === Point
    Processed: 0.012, SQL: 9