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. 优秀的图

2. 解释

function Foo ( )

function Object ( )

function Function ( )

3. 画图


一、对象的原型

1. 概念

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

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

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

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

01 - 方式一 : ( 不常用 )

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

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

const obj = {
  name: 'star'
};
console.log(obj);

// 可能存在兼容问题,因为是浏览器添加的属性,官方并没有加上
// 获取对象的原型
console.log(obj.__proto__); // 顶层Object的显式原型

// 设置对象的原型
obj.__proto__ = {info: 'aaa'}

02 - 方式二 : ( 常用 )

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

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

const obj = {
  name: 'star'
};
console.log(obj);

// 官方提供的获取原型的方法,不存在兼容问题,nice
console.log(Object.getPrototypeOf(obj)); // 顶层Object的显式原型
// 是相同的,指向同一个对象
console.log(obj.__proto__ === Object.getPrototypeOf(obj)); // true

// setPrototypeOf 这个是官方后面加的,可能有兼容问题
Object.setPrototypeOf(obj,{age:123})
// 相当于
obj.__proto__ = {age:123}

二、函数的原型

1. 概念

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

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

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

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

const obj = {}
function foo() {}

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

2. 作用

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

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

function Foo() {
  /**
   * 1. 创建空的对象
   * 2. 将Foo的prototype原型(显式隐式)赋值给空的对象的__proto__(隐式原型)      看这里
   * 3. 将this指向该对象
   * 4. 执行函数中的代码
   * 5. 如果没有明确的返回一个非空对象, 那么this指向的对象会自动返回
  */
}
// 函数的显示原型
console.log(Foo.prototype)

// new操作
const f1 = new Foo()
const f3 = new Foo()
// 实例的隐式原型 指向 函数的显式原型
console.log(f1.__proto__)
console.log(f1.__proto__ === Foo.prototype) // true

// 实例的隐式原型都共同指向构造函数的显式原型
console.log(f1.__proto__ === f3.__proto__) // true

02 - 绑定方法

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

                代码

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

                解释 

        现在 : 将方法放在原型上

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

                代码 

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

                解释

3. constructor属性 

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

1. 栗子 🌰

function Student(age) {
  this.age = age;
}

console.log(Student.prototype); // {constructor: ƒ}   拥有一个constructor
console.log(Student.prototype.constructor); // ƒ Student() {}

console.log(Student.prototype.constructor === Student); // true

const s = new Student(17);
console.log(s.age); // 17
console.log(s.__proto__.constructor === Student); // // true

2. 解释 

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

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

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

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

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

四、JavaScript原型链

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

手写原型链 ⛓️

01 - 代码

const obj = {
  name: 'star',
  age: 18
};

obj.__proto__ = {};
obj.__proto__.__proto__ = {};
obj.__proto__.__proto__.__proto__ = { message: '找到我啦' };

console.log(obj.message); // 找到我啦

02 - 解释


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

1.通过原型链实现继承

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

01 - 代码

// 1. 创建父类对象
function Person(name, friend) {
  this.name = name;
  this.friend = friend;
}
Person.prototype.eating = function () {
  console.log('eating');
};
// 2. 创建子类对象
function Student(age) {
  this.age = age;
}

/**
 * 错误写法
 * Student.prototype = Person.prototype
 * 这样写的话,Student类中的私有方法也会加到Person.prototype中
 */
// 3. 给子类的显式原型赋值,赋值为父类的一个实例
// new Peoson(),这个对象的__proto__指向Person.prototype, 构建了原型链条
const p = new Person('ppp', [{ name: 'cobe' }])
Student.prototype = p;

/**
 * 注意 : 3 和 4 位置不能互换,否则会覆盖
 */

// 4. 给子类显式原型上添加方法
Student.prototype.studing = function () {
  console.log('studing');
};

const s = new Student(18);
const s2 = new Student(20);
// 可以调用父类方法
s.eating(); // eating
// 打印的是父类实例上的name属性,和s2的共用
console.log(s.name); // ppp
// 给自己的对象上增加name属性
s.name = 'my name is star';
// 此时才有自己的name属性
console.log(s); // Student {age: 18, name: 'my name is star'}
// 没有自己的name属性
console.log(s2); // Student {age: 20}

// 也就是公用同一个friend,因为是引用类型,所以数据相互串通,完美!!!
console.log(s.friend === s2.friend); // treu
s.friend[0].age = 10;
console.log(s2.friend[0].age); // 10

02 - 解释

03 - 优缺点

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

缺点:

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

2. 借用构造函数继承

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

01 - 代码

// 创建父类对象
function Person(name, age, friend) {
  this.name = name;
  this.age = age;
  this.friend = friend;
  this.runing = function () {
    console.log(this.name + ' runing');
  };
}
// 给父类原型上添加属性
Person.prototype.commonMessage = '我是人类';
// 给父类原型上绑定方法
Person.prototype.eating = function () {
  console.log(this.name + ' eating');
};
// 创建子类对象
function Student(name, age, friend, sno) {
  // 至关重要的一步,用this去调父类的构造方法
  // 至关重要的一步,用this去调父类的构造方法
  Person.call(this, name, age, friend);
  this.sno = sno;
}

// 给子类显式原型上添加方法
Student.prototype.studing = function () {
  console.log(this.name + ' studing');
};

// 1. 可以定义个性化属性
const s1 = new Student('star', 18, [{ name: 'coder' }], 'sno0001');
const s2 = new Student('coder', 20, [{ name: 'why' }], 'sno0002');
console.log(s1); // Student {name: 'star', age: 18, friend: Array(1), sno: 'sno0001'}
console.log(s2); // Student {name: 'coder', age: 20, friend: Array(1), sno: 'sno0002'}
// 2. 调用自己类的原型方法没有问题
s1.studing(); // star studing
s2.studing(); // coder studing
// 3. 可以访问父类写在构造函数中的方法
s1.runing(); // star runing

// 4. 问题来了,访问不到父类的原型上的属性和方法
console.log(s1.commonMessage); // undefined
s1.eating(); // 报错

02 - 解释

03 - 优缺点

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

缺点:

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

3. 组合继承

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

主要代码 : 

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

01 - 代码

// 创建父类对象
function Person(name, age, friend) {
  this.name = name;
  this.age = age;
  this.friend = friend;
  this.info = 'abababab';
}
// 给父类原型上添加属性
Person.prototype.commonMessage = '我是人类';
// 给父类原型上绑定方法
Person.prototype.eating = function () {
  console.log(this.name + ' eating');
};
// 创建子类对象
function Student(name, age, friend, sno) {
  // 至关重要的第一步,用this去调父类的构造方法
  // 至关重要的第一步,用this去调父类的构造方法
  Person.call(this, name, age, friend);
  this.sno = sno;
}
// 至关重要的第二步,给子类的显式原型赋值,赋值为父类的一个实例
// 至关重要的第二步,给子类的显式原型赋值,赋值为父类的一个实例
Student.prototype = new Person();

// 给子类显式原型上添加方法
Student.prototype.studing = function () {
  console.log(this.name + ' studing');
};

// 1. 可以定义个性化属性
const s1 = new Student('star', 18, [{ name: 'coder' }], 'sno0001');
const s2 = new Student('coder', 20, [{ name: 'why' }], 'sno0002');
console.log(s1); // Student {name: 'star', age: 18, friend: Array(1), info: 'abababab', sno: 'sno0001'}
// 2. 调用自己类的原型方法没有问题
s1.studing(); // star studing
// 3. 调用父类的原型方法没有问题
s1.eating(); // star runing
// 4. 访问父类的原型上的属性没有问题
console.log(s1.commonMessage); // 我是人类

// 问题来了
// 1. 因为是给子类的显式原型赋值,赋值为父类的一个实例,所以没有传参数,那么值都为undefined,很不优雅
console.log(Student.prototype); // {name: undefined, age: undefined, friend: undefined, info: 'abababab', studing: ƒ}
// 2. 调用了两次父类构造函数

02 - 解释

03 - 优缺点

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

缺点:

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

🌟 优化中间层

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

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

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

01 - 方式一 : 之前的做法

// 之前的做法: 但是不想要这种做法

const p = new Person()
Student.prototype = p

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

const obj = {}

// 可能有兼容性问题
// obj.__proto__ = Person.prototype
// 使用官方的定义原型的方法
Object.setPrototypeOf(obj, Person.prototype)

Student.prototype = obj

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

function F() {}
F.prototype = Person.prototype

Student.prototype = new F()

04 - 方式四 : 使用 Object.create

const obj = Object.create(Person.prototype)

console.log(obj.__proto__ === Person.prototype) // true

Student.prototype = obj

05 - 创建寄生函数

        使用方式二

function Person() {}
function Student() {}

// 寄生式函数
function inherit(Subtype, Supertype) {
  // 方式二
  // 相当于 => Subtype.prototype.__proto__ = Supertype.prototype
  Object.setPrototypeOf(Subtype.prototype, Supertype.prototype)
  
  // 使得子类的显式原型上有constructor,并指向子类自己
  Object.defineProperty(Subtype.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Subtype
  });
}

inherit(Student, Person);

        使用方式三

function Person() {}
function Student() {}

// 创建对象的过程
function createObject(o) {
  function F() {}
  F.prototype = o;
  // new F().__proto__ === Supertype.prototype
  return new F();
}
// 寄生式函数
function inherit(Subtype, Supertype) {
  // 方式三
  // 相当于 Subtype.prototype => 找到new F()对象 => 通过new F().__proto__ 找到Supertype.prototype
  Subtype.prototype = createObject(Supertype.prototype);
  // 使得子类的显式原型上有constructor,并指向子类自己
  Object.defineProperty(Subtype.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Subtype
  });
}

inherit(Student, Person);

        使用方式四

function Person() {}
function Student() {}

// 寄生式函数
function inherit(Subtype, Supertype) {
  // 方式四
  // 相当于 => Subtype.prototype.__proto__ = Supertype.prototype
  Subtype.prototype = Object.create(Supertype.prototype)
  // 使得子类的显式原型上有constructor,并指向子类自己
  Object.defineProperty(Subtype.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Subtype
  });
}

inherit(Student, Person);

4. 寄生组合式继承

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

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

01 - 代码

// 寄生式函数
function inherit(Subtype, Supertype) {
  // Subtype.prototype.__proto__ = Supertype.prototype // 继承实例方法
  Subtype.prototype = Object.create(Supertype.prototype);
  Object.defineProperty(Subtype.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Subtype
  });
  // Subtype.__proto__ = Supertype. 继承类方法
  Object.setPrototypeOf(Subtype, Supertype);
}

// 创建父类对象
function Person(name, age, friend) {
  this.name = name;
  this.age = age;
  this.friend = friend;
  this.info = 'abababab';
}
// 给父类添加静态方法
Person.myPer = function () {
  console.log('myPer');
};
// 给父类原型上添加属性
Person.prototype.commonMessage = '人类';
// 给父类原型上绑定方法
Person.prototype.eating = function () {
  console.log(this.name + ' eating');
};

// 创建子类对象
function Student(name, age, friend, sno) {
  // 至关重要的第一步,用this去调父类的构造方法
  // 至关重要的第一步,用this去调父类的构造方法
  Person.call(this, name, age, friend);
  this.sno = sno;
}
// 至关重要的第二步,实现子类继承父类
// 至关重要的第二步,实现子类继承父类
inherit(Student, Person);
// 给子类添加静态方法
Student.myStu = function () {
  console.log('myStu');
};
// 给子类显式原型上添加方法
Student.prototype.studing = function () {
  console.log(this.name + ' studing');
};

// 1. 可以定义个性化属性
const s1 = new Student('star', 18, [{ name: 'coder' }], 'sno0001');
const s2 = new Student('coder', 20, [{ name: 'why' }], 'sno0002');
console.log(s1); // Student {name: 'star', age: 18, friend: Array(1), info: 'abababab', sno: 'sno0001'}
// 2. 调用自己类的原型方法没有问题
s1.studing(); // star studing
// 3. 调用父类的原型方法没有问题
s1.eating(); // star runing
// 4. 访问父类的原型上的属性没有问题
console.log(s1.commonMessage); // 人类

// 5. 连接成功
console.log(Student.prototype); // Person {studing: ƒ, constructor: ƒ}
// 6. 指向了自己
console.log(Student.prototype.constructor); // Student(name, age, friend, sno) {}
// 7. 调用自己的类方法
Student.myStu(); // myStu
// 8. 调用父类方法
Student.myPer(); // myPer

02 - 解释​​​​​​​

03 - 优缺点 

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

缺点:我也还不知道~

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

六、Object是所有类的父类

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

七、判断属性在哪的方法

栗子 🌰

const obj = {
  name: 'star',
  age: '19'
};
obj.__proto__ = {
  address: '北京'
};

console.log(obj, obj.address); // {name: 'star', age: '19'} '北京'

hasOwnProperty

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

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

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

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

// 只要能找到,不管在自己身上还是原型上,都返回true
console.log('name' in obj);
console.log('address' in obj);
// 找不到的为false
console.log('abc' in obj);

// 注意: for in遍历不仅仅是自己对象上的内容, 也包括原型对象上的内容
// 因为Object上的属性都是不可遍历的,所以才显示不出来,不是没有去找

for (var key in obj) {
  console.log(key);
}

instanceof

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

判断对象和类之间的关系,看这个对象是不是这个类的实例

// instanceof用于判断对象和类(构造函数)之间的关系
function Person() {}
function Student() {}
inherit(Student, Person)

// stu实例(instance)对象
var stu = new Student()
console.log(stu instanceof Student) // true
console.log(stu instanceof Person) // true
console.log(stu instanceof Object) // true
console.log(stu instanceof Array) // false

isPrototypeOf

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

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

function Person() {}
function Student() {}
inherit(Student, Person)

console.log(Student.prototype.isPrototypeOf(stu)) // true
console.log(Person.prototype.isPrototypeOf(stu)) // true

八、原型继承关系 

1. 优秀的图

2. 解释

function Foo ( )

  • 作为函数来说,有自己的显示原型prototype对象
    • 显示原型对象相当于是被new Object()创建出来的
    • 所以又有自己的__proto__,指向Object的显示原型
  • 作为对象来说,有自己的隐式原型__proto__对象
    • function Foo() 相当于 被 new Function () 创建出来的
    • 所以 function Foo().__proto__,指向Function函数的显示原型

function Object ( )

  • 作为函数来说,有自己的显示原型prototype对象
    • 这里比较特殊,因为是顶层了,所以显示原型对象的 __proto__ 指向null
  • 作为对象来说,有自己的隐式原型__proto__对象
    • function Object也相当于 被 new Function () 创建出来的
    • 所以 function Object().__proto__,指向Function函数的显示原型

function Function ( )

  • 作为函数来说,有自己的显示原型prototype对象
    • ​​​​​​​显示原型对象相当于是被new Object()创建出来的
    • 所以又有自己的__proto__,指向Object的显示原型
  • 作为对象来说,有自己的隐式原型__proto__对象
    • ​​​​​​​这里比较特殊,因为相当于它被自己创建了
    • function Function 相当于 被 new Function() 创建的
    • 所以function Function().__proto__ 也指向Function函数的显示原型

3. 画图

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值