likes
comments
collection
share

JS:原型-原型链-ES5继承

作者站长头像
站长
· 阅读数 52

1. 普通对象的原型

  1. 什么是对象原型?
  2. 获取对象原型的方法?
  3. 对象原型的作用?

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

  • 获取对象原型 两种方法:
  1. 不能这样获取obj.[[prototype]]
  2. 这样获取obj.__proto__

       使用的时候需要判断一下。

        这个是浏览器实现的。对象原型,也可以称为隐式原型。因为一般不常用。

        下文为了方便使用__proto__

   3. 标准的方法Object.getPrototypeOf(obj) 推荐使用标准的方法

  • 疑问:这个原型有什么用呢?

当我们通过[[get]]的方式获取一个属性对应的value时,

它会优先在自己的对象中查找,如果找到直接返回。

如果没有找到,那么会在原型对象中找。

2. 函数对象的原型

  1. 什么是函数的显式原型,和对象原型的区别?

  2. 函数原型的作用?

  3. 案例Student,将所有的函数定义放到了显式原型上。

  4. 将函数看成是一个普通对象的时候,具备[[prototype]]属性。

  5. 将函数看成是一个函数时,具备prototype属性。

对象没有prototype,可以称为显式原型,一般直接用的。

是因为函数才有prototype,不是因为是对象。

对象的原型作用:

  • 查找key对应的value时,会找到原型身上。

函数原型的作用:

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

再看new操作符

  1. 创建一个空对象

  2. 将这个空对象赋值给this

  3. 将函数的显式原型赋值给这个对象作为它的隐式原型。

  4. 执行函数体中的代码

  5. 将这个对象默认返回。

    function Foo() { // 1. 创建空的对象 // 2. 将Foo的prototype(显式)赋值给空的对象的__proto__(隐式) } console.log(Foo.prototype)

    var f1 = new Foo() var f2 = new Foo()

    console.log(f1.proto) console.log(f1.proto === Foo.prototype) // true console.log(f1.proto === f2.proto) // true 构造出来的对象的隐式原型都是同一个对象Foo.prototype。

new第三步意味着什么?

         意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype

疑问:这个同一个对象有什么作用?

       当我们多个对象用于共同的值时,我们可以将它放到构造函数对象的显示原型上,

由构造函数创建出来的所有对象,都会共享这些属性。

function Studnet(name, age) {
  this.name = name
  this.age = age
  this.studying = function() {
    console.log(this.name + " is studying")
  }
}

var stu1 = new Student("Kobe", 18)
var stu2 = new Studnet("Curry", 18)

console.log(stu1.studying === stu2.studying) 
// false,重新创建了函数,我们不需要不同的函数。

// 因为隐式原型就是Student.prototype
Student.prototype.studying = function() {
  console.log(this.name + " is studying")
}

stu1.studying()
// 通过stu1隐式原型找到studying方法。this指向stu1(隐式绑定)

3. 函数原型的属性

  1. constructor

  2. 操作属性

  3. 结合内存图理解操作

    function Person(name, age) { this.name = name this.age = age }

    Person.prototype.running = function() { console.log("running~") }

    console.log(Person.prototype.constructor === Person) // true

    var p1 = new Person("Kobe", 18) var p2 = new Person("Curry", 30)

    // 进行操作 console.log(p1.name) console.log(p2.name)

    p1.running() p2.running()

    // 新增属性 Person.prototype.address = "中国" p2.isAdmin = true // 添加p2的属性,原型对象上无isAdmin

    // 获取属性 console.log(p1.address)

    console.log(p1.isAdmin) // undefined console.log(p2.isAdmin)

    // 修改address p1.address = "广州" console.log(p2.address) // 中国

总结:

  1. 构造函数上面有prototype属性,它指向构造函数的显式原型对象,显式原型对象中有constructor属性,指向了构造函数。
  2. 所有由new关键字创建的对象的[[prototype]]属性,指向构造函数的显式原型对象,在显式原型对象中每个添加的方法属性,指向对应的方法对象。

4. 重写原型对象

使用场景:如果我们需要在原型上添加过多的属性,通常重写原型对象。

Person.prototype.message = "Hello World"
Person.prototype.info = {name: "wang", age: 18}
Person.prototype.running = function() {console.log("running~")}

/// 让构造函数的显式原型不再指向原有的默认指向的原型对象,指向一个新的空对象
Person.prototype = {
  message: "Hello World",
  info: {name: "wang", age: 18},
  running: function() {console.log("running~")},
  constructor: Person
}

console.log(Object.keys(Person.prototype))
// 这个constructor是可以枚举的。直接添加不可枚举,如果需要一致
Object.defineProperty(Person.prototype, "constructor", {
  configurable: true,
  enumerable: false,
  value: Person,
  writable: true
})

如果不写constructor: Person,这个constructor属性,会指向Object构造函数。

constructor默认属性描述符:

JS:原型-原型链-ES5继承

5. 面向对象的三大特性

  1. 概念
  2. 继承的思想

JavaScript支持面向对象编程也支持函数式编程。

面向对象编程 -->编程方式(范式)

怎么去创建一个类,根据这个类创建对象,然后操作。

  • 面向对象三大特性

  • 封装:将属性和方法封装到一个类中,可以称之为封装的过程。

  • 继承:可以减少重复的代码量,也是多态的前提(纯面向对象中)。

  • 多态:不同的对象在执行时表现出不同的形态。

  1. 将普通变量封装到一个对象 ,将一个对象封装到一个类。最终抽象为类。

  2. 不同的类,具有相同的属性和方法,可以抽取到一个父类中。子类继承父类就可以实现父类的属性。

  3. 多态在js中更多表现是一个“鸭子类型”

    function Student(name, age, score) { this.name = name this.age = age this.score = score } Student.prototype.running = function() {} Student.prototype.fighting = function() {}

    function Teacher(name, age, salary) { this.name = name this.age = age this.salary = salary } Teacher.prototype.running = function() {} Teacher.prototype.teaching = function() {}

    // 抽取父类

要实现继承,es5中需要用到原型链。

6. 对象的原型链

  1. 定义
  2. 默认原型链,自定义原型链
  3. 查找顺序,结合图
  4. 补充

定义:

  • 从一个对象上获取属性,如果当前对象中没有获取到就会去它的原型上获取。
  1. 默认原型链

  2. 自定义的原型链

    var obj = { name: "wang", age: 18 } console.log(obj.message) obj.proto = { // message: "hello a" } obj.proto.proto = { // message: "hello b" } obj.proto.proto.proto = { message: "hello c" } console.log(obj.message) console.log(obj.proto.proto.proto.proto === Object.prototype) console.log(obj.proto.proto.proto.proto.proto === null)

查找顺序

  1. obj上面查找
  2. obj.__proto__上面查找
  3. obj.__proto__.__proto__==>null 上面查找 undefined

补充:

原型链的顶层:null

所有构造函数继承Object构造函数。即Object是所有类的一个父类

7. 原型链实现继承ES5继承

function Student(name, age, score) {
  this.score = score
}
Student.prototype.fighting = function() {}

function Person(name, age) {
  this.name = name 
  this.age = age
}
Person.prototype.running = function() {
  console.log("running~")
}

var stu1 = new Student()

// 需要stu1调用running,自身的原型上去找,没有,再去原型的原型上找,指向的是Person.prototype

// 如何让二者关联?

方式一:父类的原型直接赋值给子类的原型 (错误的做法)

缺点:父类和子类共享一个原型对象,修改一个,另外一个也被修改。

Student.prototype = Person.prototype

Student.prototype.studying = function() {
  console.log("studying~")
}
var p = new Person()
p.studying()  // 可以调用

JS:原型-原型链-ES5继承

方式二:创建一个父类的实例对象,用这个实例作为子类的原型对象。

var p1 = new Person("wang", 18)
Student.prototype = p1

解决了方式一缺点,

缺点:

继承了实例对象的属性。

如果删除子类中的赋值属性的代码,也会打印出属性,

但是打印的是继承父类的属性。

JS:原型-原型链-ES5继承

原型链继承的弊端:

  1. 某些属性其实是保存在p1对象上的;
  2. 直接打印对象看不到这个属性。
  3. 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题。
  4. 不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没办法定制化)。

8. 借用构造函数实现-属性继承

  • 为了解决原型链继承中存在的问题,开发人员提供了一种新的技术:constructor stealing(借用构造函数)

做法:在子类的构造函数内部调用父类的构造函数

function Student(name, age, score) {
  Person.call(this, name, age) // 借用构造函数,调用时是Student对象
  this.score = score
}
Student.prototype.fighting = function() {}

function Person(name, age) {
  this.name = name 
  this.age = age
}
Person.prototype.running = function() {
  console.log("running~")
}

var p1 = new Person("wang", 18)
Student.prototype = p1

var stu1 = new Student("Kobe", 18, 100)
console.log(stu1.age)
console.log(stu1.name)
console.log(stu1)

加上原型链继承就是组合借用继承。

缺点:

组合继承最大的问题就父类的构造函数至少会被调用两次。

事实上,所有的子类实例事实上会拥有两份的父类属性。一个是本身,一个是原型上。

JS:原型-原型链-ES5继承

JS:原型-原型链-ES5继承

9. 最终继承方案-寄生组合式继承

9.1 原型式继承

  • 原型式继承的渊源:

  • 这种模式需要从 Douglas Crockford( 著名的前端大师,JSON的创立者 )在2006年写的一篇文章说起: Prototypal Inheritance in JavaScript

  • 它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的

  • 最终的目的

student对象的原型指向了person对象

9.2 寄生式继承

  • 寄生式(Parasitic)继承

  • 寄生式继承是与原型式继承紧密相关的一种思想,并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;

  • 寄生式继承的思路是结合原型类继承和工厂模式的一种方式;

  • 即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;

需要创建一个类似于p1的对象满足

  1. 必须创建出来一个对象

  2. 将这个对象的隐式原型属性指向父类的显式原型属性

  3. 将这个对象赋值给子类的显式原型

    function Person() {

    } function Student() {

    } // 之前的做法 // var p = new Person() // Studnet.prototype = p // 方案一 // var obj = {} // Object.setPrototypeOf(obj, Person.prototype) // Student.prototuype = obj

    // 方案二 // function F() {} // F.prototype = Person.prototype // Student.Prototype = new F()

    // 方案三 // var obj = Object.create(Person.prototype) // console.log(obj.proto === Person.prototype) // Student.prototype = obj

    // 封装工具函数 function inherit(Subtype, Supertype) { Subtype.prototype = Object.create(Supertype.prototype) Object.defineProperty(Subtype.prototype, "constructor", { enumerable: false, configurable: true, wriable: true, value: Subtype }) }

    inherit(Student, Person)

    // 考虑兼容性问题 Object.create function createObject(o) { function F() {} F.prototype = o return new F() }

将Subtype和Supertype联系在一起,称为寄生式函数。

整个过程称为寄生组合式继承。

9.3 寄生组合式继承

寄生组合式继承:

  1. 原型链

  2. 借用构造函数

  3. 原型式(对象之间)

  4. 寄生式函数

    function inherit(Subtype, Supertype) { Subtype.prototype = Object.create(Supertype.prototype) Object.defineProperty(Subtype.prototype, "constructor", { enumerable: false, configurable: true, wriable: true, value: Subtype }) } function Person(name, age) { this.name = name this.age = age } Person.prototype.running = function() { console.log("running~") }

    function Student(name, age, score) { Person.call(this, name, age) this.score = score }

    inherit(Student, Person) Student.prototype.studying = function() { console.log("studying~") }

    var stu1 = new Student("Kobe", 30, 10000000) console.log(stu1) stu1.studying() stu1.running()

JS:原型-原型链-ES5继承

完美解决

11. 原型-寄生思想的来源

Douglas Crockford研究对象之间的继承。实际上实现继承并不好。

var obj = {
  name: "wang",
  age: 18
}

// 原型式继承
var info = {}
info.__proto__ = obj
// 如果创建多个继承的对象,出现大量重复代码

// 使用原型式继承  __proto__会出现浏览器兼容性问题,因此没有用
function createObject(o) {
  function F() {}  
  F.prototype = o
  return new F()
}

// var info1 = createObject(obj)
// info1.height = 188
// var info2 = createObject(obj)
// info2.height = 199
// 目前的代码有弊端,没有自己特有的属性,需要单独添加。
// 寄生式函数
function createInfo(o, name, age, height) {
  var newObj = createObject(o)
  newObj.name = name
  newObj.age = age
  newObj.height = height
  return newObj
}

// 创建一系列对象
var info1 = createInfo(obj, "Kobe", 18, 222)
console.log(info1)

10. 原型继承关系

JS:原型-原型链-ES5继承

内存图:

JS:原型-原型链-ES5继承

转载自:https://juejin.cn/post/7144689665562378277
评论
请登录