• JS高级 之 ES5 实现继承


    目录

    一、对象的原型

    1. 概念

    2. 获取|设置对象的原型

    01 - 方式一 : ( 不常用 )

    02 - 方式二 : ( 常用 )

    二、函数的原型

    1. 概念

    2. 作用

    01 - 回顾下new操作符做的事

    02 - 绑定方法

            以前  : 直接在构造函数中写

                    解释 

            现在 : 将方法放在原型上

                    代码 

                    解释

    3. constructor属性 

    1. 栗子 🌰

    2. 解释 

    三、面向对象的特性 – 继承

    四、JavaScript原型链

    手写原型链 ⛓️

    01 - 代码

    02 - 解释

    五、实现继承的方式 => 重点来了

    1.通过原型链实现继承

    01 - 代码

    02 - 解释

    03 - 优缺点

    2. 借用构造函数继承

    01 - 代码

    02 - 解释

    03 - 优缺点

    3. 组合继承

    01 - 代码

    02 - 解释

    03 - 优缺点

    🌟 优化中间层

    01 - 方式一 : 之前的做法

    02 - 方式二 : 使用 Object.setPrototypeOf方法

    03 - 方式三 : 使用函数,拐个弯

    04 - 方式四 : 使用 Object.create

    05 - 创建寄生函数

            使用方式二

            使用方式三

            使用方式四

    4. 寄生组合式继承

    01 - 代码

    02 - 解释

    03 - 优缺点 

    六、Object是所有类的父类

    七、判断属性在哪的方法

    栗子 🌰

    hasOwnProperty

    ​​​​​​in / for in 操作符

    instanceof

    isPrototypeOf


    一、对象的原型

    1. 概念

    JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象

    • 当通过引用对象的属性key来获取一个value时,它会触发 [[ Get ]]的操作
    • 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它
    • 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性

    只要是对象都会有这样的一个内置属性

    2. 获取|设置对象的原型

    01 - 方式一 : ( 不常用 )

    通过对象的 __proto__ 属性可以获取 | 设置

    tip : 但是这个是早期浏览器自己添加的,存在一定的兼容性问题

    1. const obj = {
    2. name: 'star'
    3. };
    4. console.log(obj);
    5. // 可能存在兼容问题,因为是浏览器添加的属性,官方并没有加上
    6. // 获取对象的原型
    7. console.log(obj.__proto__); // 顶层Object的显式原型
    8. // 设置对象的原型
    9. obj.__proto__ = {info: 'aaa'}

    02 - 方式二 : ( 常用 )

    获取 : 通过 Object.getPrototypeOf 方法可以获取

    设置 : 通过 Object.setPrototypeOf 方法可以设置

    1. const obj = {
    2. name: 'star'
    3. };
    4. console.log(obj);
    5. // 官方提供的获取原型的方法,不存在兼容问题,nice
    6. console.log(Object.getPrototypeOf(obj)); // 顶层Object的显式原型
    7. // 是相同的,指向同一个对象
    8. console.log(obj.__proto__ === Object.getPrototypeOf(obj)); // true
    9. // setPrototypeOf 这个是官方后面加的,可能有兼容问题
    10. Object.setPrototypeOf(obj,{age:123})
    11. // 相当于
    12. obj.__proto__ = {age:123}

    二、函数的原型

    1. 概念

    所有的函数都有一个prototype的属性(注意:不是__proto__)

    箭头函数没有,箭头函数没有原型,箭头函数是ES6提出的,同时期提出的还有class,注意

    虽然函数也是一个对象,但是对象上面没有prototype的属性

    因为是函数,所以才有这个属性

    1. const obj = {}
    2. function foo() {}
    3. // 作用: 用来构建对象时, 给对象设置隐式原型的
    4. console.log(foo.prototype) // {constructor: ƒ}
    5. // console.log(obj.prototype) 对象是没有prototype

    2. 作用

    作用 : 用来构建对象时, 给对象设置隐式原型的

    01 - 回顾下new操作符做的事

    1. function Foo() {
    2. /**
    3. * 1. 创建空的对象
    4. * 2. 将Foo的prototype原型(显式隐式)赋值给空的对象的__proto__(隐式原型) 看这里
    5. * 3. 将this指向该对象
    6. * 4. 执行函数中的代码
    7. * 5. 如果没有明确的返回一个非空对象, 那么this指向的对象会自动返回
    8. */
    9. }
    10. // 函数的显示原型
    11. console.log(Foo.prototype)
    12. // new操作
    13. const f1 = new Foo()
    14. const f3 = new Foo()
    15. // 实例的隐式原型 指向 函数的显式原型
    16. console.log(f1.__proto__)
    17. console.log(f1.__proto__ === Foo.prototype) // true
    18. // 实例的隐式原型都共同指向构造函数的显式原型
    19. console.log(f1.__proto__ === f3.__proto__) // true

    02 - 绑定方法

            以前  : 直接在构造函数中写

                    代码

    1. function Student(name, age, sno) {
    2. this.name = name;
    3. this.age = age;
    4. this.sno = sno;
    5. // 这样也可以,但是相当于每个对象创建了自己的方法,大大占用了内存空间
    6. this.running = function () {
    7. console.log(this.name + ' running');
    8. };
    9. this.eating = function () {
    10. console.log(this.name + ' eating');
    11. };
    12. }
    13. // 每个实例对象都创建了函数,其实是没有必要的
    14. const s1 = new Student('star',16,1)
    15. const s2 = new Student('coder',17,2)
    16. const s3 = new Student('coderstar',18,3)

                    解释 

            现在 : 将方法放在原型上

    • 当多个对象拥有共同的值 ( 一般指函数 ) 时, 我们可以将它放到构造函数对象的显式原型
    • 由构造函数创建出来的所有对象, 都会共享这些属性

                    代码 

    1. function Student(name, age, sno) {
    2. // 1. 这些属性是自己的,所以写在这里
    3. this.name = name;
    4. this.age = age;
    5. this.sno = sno;
    6. }
    7. // 2. 函数是共享的,每个实例都需要这个函数,为了减少空间,可以把方法挂载到构造函数的显式原型上面
    8. Student.prototype.running = function () {
    9. console.log(this.name + ' running');
    10. };
    11. Student.prototype.eating = function () {
    12. console.log(this.name + ' eating');
    13. };
    14. // 3. 创建对象
    15. const s1 = new Student('star', 16, 1);
    16. const s2 = new Student('coder', 17, 2);
    17. const s3 = new Student('coderstar', 18, 3);
    18. // 4. 当调用方法的时候,如果自己对象中没有,会去隐式原型中查找
    19. // 5. 而对象的隐式原型就是函数的显示原型 s1.__proto === Student.prototype
    20. // 6. 所以可以使用
    21. s1.eating()
    22. s2.running()

                    解释

    3. constructor属性 

    函数的显式原型上都会存在一个属性叫做constructor,这个constructor指向当前的函数对象

    1. 栗子 🌰

    1. function Student(age) {
    2. this.age = age;
    3. }
    4. console.log(Student.prototype); // {constructor: ƒ} 拥有一个constructor
    5. console.log(Student.prototype.constructor); // ƒ Student() {}
    6. console.log(Student.prototype.constructor === Student); // true
    7. const s = new Student(17);
    8. console.log(s.age); // 17
    9. console.log(s.__proto__.constructor === Student); // // true

    2. 解释 

    三、面向对象的特性 – 继承

    面向对象有三 ( 四 ) 大特性:封装、继承、多态、( 抽象 )

    • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
    • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态的前提(纯面向对象中)
    • 多态:不同的对象在执行时表现出不同的形态

    继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可

    JavaScript当中实现继承 => 使用JavaScript原型链的机制

    四、JavaScript原型链

    从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取

    手写原型链 ⛓️

    01 - 代码

    1. const obj = {
    2. name: 'star',
    3. age: 18
    4. };
    5. obj.__proto__ = {};
    6. obj.__proto__.__proto__ = {};
    7. obj.__proto__.__proto__.__proto__ = { message: '找到我啦' };
    8. console.log(obj.message); // 找到我啦

    02 - 解释


    五、实现继承的方式 => 重点来了

    1.通过原型链实现继承

    主要代码 : 子类构造函数.prototype = new 父类构造函数( )

    01 - 代码

    1. // 1. 创建父类对象
    2. function Person(name, friend) {
    3. this.name = name;
    4. this.friend = friend;
    5. }
    6. Person.prototype.eating = function () {
    7. console.log('eating');
    8. };
    9. // 2. 创建子类对象
    10. function Student(age) {
    11. this.age = age;
    12. }
    13. /**
    14. * 错误写法
    15. * Student.prototype = Person.prototype
    16. * 这样写的话,Student类中的私有方法也会加到Person.prototype中
    17. */
    18. // 3. 给子类的显式原型赋值,赋值为父类的一个实例
    19. // new Peoson(),这个对象的__proto__指向Person.prototype, 构建了原型链条
    20. const p = new Person('ppp', [{ name: 'cobe' }])
    21. Student.prototype = p;
    22. /**
    23. * 注意 : 3 和 4 位置不能互换,否则会覆盖
    24. */
    25. // 4. 给子类显式原型上添加方法
    26. Student.prototype.studing = function () {
    27. console.log('studing');
    28. };
    29. const s = new Student(18);
    30. const s2 = new Student(20);
    31. // 可以调用父类方法
    32. s.eating(); // eating
    33. // 打印的是父类实例上的name属性,和s2的共用
    34. console.log(s.name); // ppp
    35. // 给自己的对象上增加name属性
    36. s.name = 'my name is star';
    37. // 此时才有自己的name属性
    38. console.log(s); // Student {age: 18, name: 'my name is star'}
    39. // 没有自己的name属性
    40. console.log(s2); // Student {age: 20}
    41. // 也就是公用同一个friend,因为是引用类型,所以数据相互串通,完美!!!
    42. console.log(s.friend === s2.friend); // treu
    43. s.friend[0].age = 10;
    44. console.log(s2.friend[0].age); // 10

    02 - 解释

    03 - 优缺点

    优点:好理解,逻辑清晰,可以继承父类属性和方法

    缺点:

    • 不好传参数,无法定制化对象,不能继承父类属性
    • 如果要定制对象,需要在自己构造函数中写,重复代码
    • 若父类有引用类型,如数组,数据可能公用导致混乱

    2. 借用构造函数继承

    主要代码 : 在子类构造函数中使用 call | apply 调用父类构造函数

    01 - 代码

    1. // 创建父类对象
    2. function Person(name, age, friend) {
    3. this.name = name;
    4. this.age = age;
    5. this.friend = friend;
    6. this.runing = function () {
    7. console.log(this.name + ' runing');
    8. };
    9. }
    10. // 给父类原型上添加属性
    11. Person.prototype.commonMessage = '我是人类';
    12. // 给父类原型上绑定方法
    13. Person.prototype.eating = function () {
    14. console.log(this.name + ' eating');
    15. };
    16. // 创建子类对象
    17. function Student(name, age, friend, sno) {
    18. // 至关重要的一步,用this去调父类的构造方法
    19. // 至关重要的一步,用this去调父类的构造方法
    20. Person.call(this, name, age, friend);
    21. this.sno = sno;
    22. }
    23. // 给子类显式原型上添加方法
    24. Student.prototype.studing = function () {
    25. console.log(this.name + ' studing');
    26. };
    27. // 1. 可以定义个性化属性
    28. const s1 = new Student('star', 18, [{ name: 'coder' }], 'sno0001');
    29. const s2 = new Student('coder', 20, [{ name: 'why' }], 'sno0002');
    30. console.log(s1); // Student {name: 'star', age: 18, friend: Array(1), sno: 'sno0001'}
    31. console.log(s2); // Student {name: 'coder', age: 20, friend: Array(1), sno: 'sno0002'}
    32. // 2. 调用自己类的原型方法没有问题
    33. s1.studing(); // star studing
    34. s2.studing(); // coder studing
    35. // 3. 可以访问父类写在构造函数中的方法
    36. s1.runing(); // star runing
    37. // 4. 问题来了,访问不到父类的原型上的属性和方法
    38. console.log(s1.commonMessage); // undefined
    39. s1.eating(); // 报错

    02 - 解释

    03 - 优缺点

    优点:可以向父类传参,定制对象,且不会造成属性共享的问题

    缺点:

    • 虽然可以继承属性和方法,但方法必须写在构造函数中,创建出来的对象都自带方法,无法进行方法的复用,浪费空间
    • 父类的原型上绑定的属性和方法无法被子类使用

    3. 组合继承

    组合继承 = 原型链继承 + 借用构造函数继承

    主要代码 : 

    • 1. 子类构造函数.prototype = new 父类构造函数( )
    • 2. 在子类构造函数中使用 call | apply 调用父类构造函数

    01 - 代码

    1. // 创建父类对象
    2. function Person(name, age, friend) {
    3. this.name = name;
    4. this.age = age;
    5. this.friend = friend;
    6. this.info = 'abababab';
    7. }
    8. // 给父类原型上添加属性
    9. Person.prototype.commonMessage = '我是人类';
    10. // 给父类原型上绑定方法
    11. Person.prototype.eating = function () {
    12. console.log(this.name + ' eating');
    13. };
    14. // 创建子类对象
    15. function Student(name, age, friend, sno) {
    16. // 至关重要的第一步,用this去调父类的构造方法
    17. // 至关重要的第一步,用this去调父类的构造方法
    18. Person.call(this, name, age, friend);
    19. this.sno = sno;
    20. }
    21. // 至关重要的第二步,给子类的显式原型赋值,赋值为父类的一个实例
    22. // 至关重要的第二步,给子类的显式原型赋值,赋值为父类的一个实例
    23. Student.prototype = new Person();
    24. // 给子类显式原型上添加方法
    25. Student.prototype.studing = function () {
    26. console.log(this.name + ' studing');
    27. };
    28. // 1. 可以定义个性化属性
    29. const s1 = new Student('star', 18, [{ name: 'coder' }], 'sno0001');
    30. const s2 = new Student('coder', 20, [{ name: 'why' }], 'sno0002');
    31. console.log(s1); // Student {name: 'star', age: 18, friend: Array(1), info: 'abababab', sno: 'sno0001'}
    32. // 2. 调用自己类的原型方法没有问题
    33. s1.studing(); // star studing
    34. // 3. 调用父类的原型方法没有问题
    35. s1.eating(); // star runing
    36. // 4. 访问父类的原型上的属性没有问题
    37. console.log(s1.commonMessage); // 我是人类
    38. // 问题来了
    39. // 1. 因为是给子类的显式原型赋值,赋值为父类的一个实例,所以没有传参数,那么值都为undefined,很不优雅
    40. console.log(Student.prototype); // {name: undefined, age: undefined, friend: undefined, info: 'abababab', studing: ƒ}
    41. // 2. 调用了两次父类构造函数

    02 - 解释

    03 - 优缺点

    优点:用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,弥补了各自的缺点

    缺点:

    • 父类至少被调用了两次,一次是call调用,一次是改变子类prototype指向时调用
    • 所有的子类实例事实上会拥有两份父类的属性

    🌟 优化中间层

    这里说的中间层就是 new Person ( )  这个对象

    想要创建一个优秀的中间层对象,需要满足几个条件 : 

    • 1. 必须创建出来一个对象
    • 2. 这个对象的隐式原型必须指向父类的显式原型
    • 3. 将这个对象赋值给子类的显式原型

    01 - 方式一 : 之前的做法

    1. // 之前的做法: 但是不想要这种做法
    2. const p = new Person()
    3. Student.prototype = p

    02 - 方式二 : 使用 Object.setPrototypeOf方法

    1. const obj = {}
    2. // 可能有兼容性问题
    3. // obj.__proto__ = Person.prototype
    4. // 使用官方的定义原型的方法
    5. Object.setPrototypeOf(obj, Person.prototype)
    6. Student.prototype = obj

    03 - 方式三 : 使用函数,拐个弯

    1. function F() {}
    2. F.prototype = Person.prototype
    3. Student.prototype = new F()

    04 - 方式四 : 使用 Object.create

    1. const obj = Object.create(Person.prototype)
    2. console.log(obj.__proto__ === Person.prototype) // true
    3. Student.prototype = obj

    05 - 创建寄生函数

            使用方式二

    1. function Person() {}
    2. function Student() {}
    3. // 寄生式函数
    4. function inherit(Subtype, Supertype) {
    5. // 方式二
    6. // 相当于 => Subtype.prototype.__proto__ = Supertype.prototype
    7. Object.setPrototypeOf(Subtype.prototype, Supertype.prototype)
    8. // 使得子类的显式原型上有constructor,并指向子类自己
    9. Object.defineProperty(Subtype.prototype, 'constructor', {
    10. enumerable: false,
    11. configurable: true,
    12. writable: true,
    13. value: Subtype
    14. });
    15. }
    16. inherit(Student, Person);

            使用方式三

    1. function Person() {}
    2. function Student() {}
    3. // 创建对象的过程
    4. function createObject(o) {
    5. function F() {}
    6. F.prototype = o;
    7. // new F().__proto__ === Supertype.prototype
    8. return new F();
    9. }
    10. // 寄生式函数
    11. function inherit(Subtype, Supertype) {
    12. // 方式三
    13. // 相当于 Subtype.prototype => 找到new F()对象 => 通过new F().__proto__ 找到Supertype.prototype
    14. Subtype.prototype = createObject(Supertype.prototype);
    15. // 使得子类的显式原型上有constructor,并指向子类自己
    16. Object.defineProperty(Subtype.prototype, 'constructor', {
    17. enumerable: false,
    18. configurable: true,
    19. writable: true,
    20. value: Subtype
    21. });
    22. }
    23. inherit(Student, Person);

            使用方式四

    1. function Person() {}
    2. function Student() {}
    3. // 寄生式函数
    4. function inherit(Subtype, Supertype) {
    5. // 方式四
    6. // 相当于 => Subtype.prototype.__proto__ = Supertype.prototype
    7. Subtype.prototype = Object.create(Supertype.prototype)
    8. // 使得子类的显式原型上有constructor,并指向子类自己
    9. Object.defineProperty(Subtype.prototype, 'constructor', {
    10. enumerable: false,
    11. configurable: true,
    12. writable: true,
    13. value: Subtype
    14. });
    15. }
    16. inherit(Student, Person);

    4. 寄生组合式继承

    寄生组合式继承 = 原型链继承 + 借用构造函数继承 + 寄生函数

    寄生函数 : 随便拿上面的一个使用即可 , 这里使用方式四的寄生函数

    01 - 代码

    1. // 寄生式函数
    2. function inherit(Subtype, Supertype) {
    3. Subtype.prototype = Object.create(Supertype.prototype);
    4. Object.defineProperty(Subtype.prototype, 'constructor', {
    5. enumerable: false,
    6. configurable: true,
    7. writable: true,
    8. value: Subtype
    9. });
    10. }
    11. // 创建父类对象
    12. function Person(name, age, friend) {
    13. this.name = name;
    14. this.age = age;
    15. this.friend = friend;
    16. this.info = 'abababab';
    17. }
    18. // 给父类原型上添加属性
    19. Person.prototype.commonMessage = '人类';
    20. // 给父类原型上绑定方法
    21. Person.prototype.eating = function () {
    22. console.log(this.name + ' eating');
    23. };
    24. // 创建子类对象
    25. function Student(name, age, friend, sno) {
    26. // 至关重要的第一步,用this去调父类的构造方法
    27. // 至关重要的第一步,用this去调父类的构造方法
    28. Person.call(this, name, age, friend);
    29. this.sno = sno;
    30. }
    31. // 至关重要的第二步,实现子类继承父类
    32. // 至关重要的第二步,实现子类继承父类
    33. inherit(Student, Person);
    34. // 给子类显式原型上添加方法
    35. Student.prototype.studing = function () {
    36. console.log(this.name + ' studing');
    37. };
    38. // 1. 可以定义个性化属性
    39. const s1 = new Student('star', 18, [{ name: 'coder' }], 'sno0001');
    40. const s2 = new Student('coder', 20, [{ name: 'why' }], 'sno0002');
    41. console.log(s1); // Student {name: 'star', age: 18, friend: Array(1), info: 'abababab', sno: 'sno0001'}
    42. // 2. 调用自己类的原型方法没有问题
    43. s1.studing(); // star studing
    44. // 3. 调用父类的原型方法没有问题
    45. s1.eating(); // star runing
    46. // 4. 访问父类的原型上的属性没有问题
    47. console.log(s1.commonMessage); // 我是人类
    48. // 5. 连接成功
    49. console.log(Student.prototype); // Person {studing: ƒ, constructor: ƒ}
    50. // 6. 指向了自己
    51. console.log(Student.prototype.constructor); // Student(name, age, friend, sno) {}

    02 - 解释

    03 - 优缺点 

    优点:这是最成熟的方法,也是现在库实现的方法

    缺点:我也还不知道~

    相信看到这里了,都懂原型链了对吧,对吧!

    六、Object是所有类的父类

    什么地方是原型链的尽头 : Object的原型对象

    七、判断属性在哪的方法

    栗子 🌰

    1. const obj = {
    2. name: 'star',
    3. age: '19'
    4. };
    5. obj.__proto__ = {
    6. address: '北京'
    7. };
    8. console.log(obj, obj.address); // {name: 'star', age: '19'} '北京'

    hasOwnProperty

    对象是否有某一个属于自己的属性(不是在原型上的属性)

    1. // 判断是否是自己的属性,是为true
    2. console.log(obj.hasOwnProperty('name')); // true
    3. console.log(obj.hasOwnProperty('address')); // false
    4. // 没有的属性也为false
    5. console.log(obj.hasOwnProperty('abc')); // false

    ​​​​​​in / for in 操作符

    判断某个属性是否在某个对象或者对象的原型上

    1. // 只要能找到,不管在自己身上还是原型上,都返回true
    2. console.log('name' in obj);
    3. console.log('address' in obj);
    4. // 找不到的为false
    5. console.log('abc' in obj);
    6. // 注意: for in遍历不仅仅是自己对象上的内容, 也包括原型对象上的内容
    7. // 因为Object上的属性都是不可遍历的,所以才显示不出来,不是没有去找
    8. for (var key in obj) {
    9. console.log(key);
    10. }

    instanceof

    用于检测构造函数(Person、Student类)的pototype,是否出现在某个实例对象的原型链上

    判断对象和类之间的关系

    1. // instanceof用于判断对象和类(构造函数)之间的关系
    2. function Person() {}
    3. function Student() {}
    4. inherit(Student, Person)
    5. // stu实例(instance)对象
    6. var stu = new Student()
    7. console.log(stu instanceof Student) // true
    8. console.log(stu instanceof Person) // true
    9. console.log(stu instanceof Object) // true
    10. console.log(stu instanceof Array) // false

    isPrototypeOf

    用于检测某个对象,是否出现在某个实例对象的原型链上

    判断对象和对象之间的关系

    1. function Person() {}
    2. function Student() {}
    3. inherit(Student, Person)
    4. console.log(Student.prototype.isPrototypeOf(stu)) // true
    5. console.log(Person.prototype.isPrototypeOf(stu)) // true

  • 相关阅读:
    【2023美团后端-8】删除字符串的方案,限制不能连续删
    Flink部署——高可用
    重读经典《操作系统:设计与实现》
    设计模式学习笔记——抽象工厂模式
    软件考试学习笔记(希赛)
    Codeforces Round 905 Div 1 (CF1887)
    软件测试:写一个好的测试用例
    y96.第六章 微服务、服务网格及Envoy实战 -- xDS API与动态配置(七)
    @Transactional注解为何会失效
    人工智能的时代---AI的影响
  • 原文地址:https://blog.csdn.net/a15297701931/article/details/125864916