美文网首页
JavaScript 面向对象编程-原型prototype

JavaScript 面向对象编程-原型prototype

作者: 梦晓半夏_d68a | 来源:发表于2020-07-13 23:04 被阅读0次

  上一篇谈到了四种创建对象的方式: 调用构造函数Object、以字面量的形式、工厂函数、构造函数。上一篇:JavaScript 面向对象编程-创建对象
  接下来我们接着谈原型 prototype

原型prototype

JavaScript 规定,每一个函数都有一个 prototype 属性,指向另一个对象。 这个对象的所有属性和方法,都会被构造函数的所拥有。

  这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。

function CreatePerson (name, gender) {
    this.name = name,
    this.gender = gender
}
CreatePerson.prototype.show = function() {
    console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
let benson = new CreatePerson('Benson', '男')
console.log(lucy.show === benson.show) // true

  与构造函数模式不同的是,lucybenson 访问的都是同一个 show 函数,节约内存,且不存在全局作用域污染问题,完美的解决了构造函数创建对象的问题,构造函数 + 原型对象 创建对象是目前创建多个对象里最广泛使用的一种方式。既然原型模式常用于创建多个对象,那么它的工作原理是什么还是值得我们好好理解一下,在理解工作原理之前还是必须先理解 ECMAScript 中原型对象。

理解原型对象

prototype、constructor 、__proto__
 (1)我们所创建的每一个函数,解析器都会给函数增加一个 prototype 属性,这个属性就对应着我们的原型对象。 (PS:如果函数作为普通函数调用,prototype 没有任何作用,prototype 属性常在涉及到构造函数时使用)
 (2)在默认情况下,每个函数的原型对象都有一个属性 constructor 构造器指向那个函数。(PS:构造函数也是函数,因此前面这句话同样适用)
 (3)当函数通过构造函数的形式调用时,它所创建的实例对象都会有一个隐含的属性指向该构造函数的原型对象,我们可以通过 __proto__ 来访问该隐含属性。

原型对象
  原型对象就相当于一个公共的区域,同一个类的实例对象都可以访问这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。

原型作用:数据共享,继承。(JS是通过原型继承的)

使用目的:节省空间

  原型对象的工作原理在介绍原型链后再介绍,详见 new 关键字执行过程内容

构造函数、实例对象、原型对象的关系

  根据上面的内容,总结构造函数、原型对象、实例对象之间的关系:

构造函数、实例对象、原型对象的关系

实例对象的方法及属性查找

  在了解了 构造函数-实例对象-原型对象 三者之间的关系后,接下来我们来看看为什么实例对象可以访问原型对象中的成员。
  每当解析器读取实例对象的某个属性或方法时,都会执行一次搜索,下面以查找属性为例,搜索顺序为:

1、自身找:首先从实例对象自身开始,自身有则返回该属性的值,自身没有则去原型对象中查找
2、原型对象找:原型对象有则返回该属性的值,原型对象没有则去原型的原型中查找
3、原型的原型中查找:直到找到Object的原型,如果没有则返回undefined,因为Object原型的原型为null

  如果创建的实例对象不想使用其原型对象上的方法,可以直接给实例对象添加方法。还是以CreatePerson为例:

  当我们调用lucy.show() 的时候,会先后执行两次搜索:

  • 首先,解析器会问:“实例对象 lucy 有 show 方法吗?”答:“没有"。

  • ”然后,它继续搜索,再问:“ lucy 的原型有 show 方法吗?”答:“有“。

  • 于是,它就读取那个保存在原型对象中的函数。

  • 当我们调用 benson.show() 时,将会重现相同的搜索过程,得到相同的结果。

这正是多个实例对象共享原型所保存的属性和方法的基本原理。

三、原型链

介绍原型链

  在理解原型对象内容里,我提到了所有构造函数创建的的实例对象都有一个隐式属性__proto__指向原型对象,在此我想补充一下:所有对象都有__proto__属性【 Object.prototypeObject对象.__proto__除外】,原型对象也是对象,所以也有__proto__ 属性,那么原型对象的__proto__ 指向谁呢?

function CreatePerson (name, gender) {
    this.name = name,
    this.gender = gender
}
CreatePerson.prototype.show = function() {
    console.log(`hello,my name is ${this.name}`)
}
let lucy1 = new CreatePerson('Lucy', '女')
console.log(CreatePerson.prototype.__proto__) // 指向Object.prototype
console.log(CreatePerson.prototype.__proto__ === Object.prototype) // true
console.log(CreatePerson.prototype.__proto__.__proto__) // null

  由执行结果发现,CreatePerson 的原型对象的__proto__指向Object的原型对象,这样一级一级向上,就构成了一个__proto__链,即原型链。当然原型链不会无限向上,它有个终点为Object.prototype(Object的原型对象的__proto__属性为null),可以称为原型链的顶端,或者root。

  下面将构造函数、实例对象、原型对象结合原型链的关系绘制出来:

原型链

  通过上面这个图写出原型链:lucy->CreatePerson.prototype->Object.prototype->null,对象的方法及属性查找都是按照原型链一步一步往上查找。这也就解释了为什么自定义的对象能调用toString, valueOf,等方法了吧?

  因为所有的对象都继承自Object

原型链方法及属性

isPrototypeOf()
  用于测试一个对象是否存在于另一个对象的原型链上。

prototypeObj.isPrototypeOf(object)

function CreatePerson (name, gender) {
    this.name = name,
    this.gender = gender
}
CreatePerson.prototype.show = function() {
    console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
console.log(CreatePerson.prototype.isPrototypeOf(lucy)) // true
console.log(Object.prototype.isPrototypeOf(lucy)) // true

  从上面的执行结果得到 CreatePerson.prototypeObject.prototype 都在 lucy 对象的原型链上 。

Object.getPrototypeOf()
  返回指定对象的原型(内部[[Prototype]]属性的值)

还是以上面的例子为例:

function CreatePerson (name, gender) {
    this.name = name,
    this.gender = gender
}
CreatePerson.prototype.show = function() {
    console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
console.log(Object.getPrototypeOf(lucy)) // CreatePerson.prototype
console.log(Object.getPrototypeOf(lucy.__proto__)) // Object.prototype

hasOwnProperty() & in操作符

hasOwnProperty():所有继承了 Object 的对象都会继承 hasOwnProperty 方法。这个方法可以用来检测一个对象是否含有特定的自身属性;和 in 操作符不同,该方法会忽略掉那些从原型链上继承到的属性。

in操作符:判断属性是否在实例或者原型对象上,只要一个满足条件,返回值都是true。

function CreatePerson (name, gender) {
    this.name = name,
    this.gender = gender
}
CreatePerson.prototype.show = function() {
    console.log(`hello,my name is ${this.name}`)
}
let lucy = new CreatePerson('Lucy', '女')
// hasOwnProperty 判断实例对象自身是否含有属性,范围小
console.log(lucy.hasOwnProperty('name')) // true
console.log(lucy.hasOwnProperty('show')) // false
console.log(lucy.hasOwnProperty('toString')) // false
console.log(lucy.hasOwnProperty('hasOwnProperty')) // false
// in 判断属性是否在实例对象或者原型对象上
console.log('name' in lucy) // true
console.log('show' in lucy) // true
console.log('toString' in lucy) // true
console.log('hasOwnProperty' in lucy) // true

  结合hasOwnProperty()in操作符,可以封装一个函数来 判断某个属性是否在原型上,是则返回true,不是返回false。

function hasPrototypeProperty( obj, name ){
    return !obj.hasOwnProperty( name ) && ( name in obj )
}

new关键字执行过程

  说起 new ,我想起一个小段子:不要跟程序员谈对象,分分钟就给你 new 出一个对象,这大概就是来自程序员的浪漫吧。哈哈,扯远了,还是进入正题:

function CreatePerson (name, gender) {
    this.name = name,
    this.gender = gender
    this.show = function () {
        console.log(`hello,my name is ${this.name}`)
    }
}
let lucy = new CreatePerson('Lucy', '女')

  上述代码中new了一个 lucy 对象,new 过程如下:

第一步:在堆中创建一个空对象,并在栈中新建一个变量 lucy,变量保存的是堆中新建的空对象的地址。let obj = new Object()
第二步:设置原型链,设置 obj 的__proto__属性指向构造函数CreatePerson的原型对象,即obj .__proto__ = CreatePerson.prototype,此时便建立了 obj 的原型链:obj->CreatePerson.prototype->Object.prototype->null
第三步:改变构造函数 CreatePerson 的 this 绑定到 obj ,并且利用 call() 或者是 apply() 来执行构造函数CreatePerson,如下:let result = CreatePerson.apply(obj, 参数) 第四步: 判断构造函数CreatePerson的返回值类型,如果构造函数返回的值是引用类型,则返回这个引用类型,否则直接返回新创建的对象。

  结合上面所说的new关键字执行过程,手写一个函数实现一下,增强理解:

function newObject(func) {
    let obj = {}
    obj.__proto__ = func.prototype
    let res = func.apply(obj, [...arguments].slice(1)) // [...arguments].slice(1) 获取传入的除第一个参数的所有实参,以数组格式
    return typeof res === 'object' ? res : obj
}

  Remark:在函数内部有个神秘的空间(arguments),这个空间会将所有的实参全部保存,不论有没有被接收,可以在函数中通过 arguments 及 arguments.length 来获取传入的实参及个数。

  封装好后,测试一下封装的 newObject 函数,看看它是否实现了和原生 new 关键字同样的功能。

// 构造函数
function CreatePerson (name, gender) {
    this.name = name,
    this.gender = gender
}
CreatePerson.prototype = {
    show: function() {
        console.log(`hello,my name is ${this.name}`)
    },
    motto: function() {
        console.log(`study hard and make progress every day`)
    }
}
// 封装调用过程
function newObject(func) {
    let obj = {}
    obj.__proto__ = func.prototype
    let res = func.apply(obj, [...arguments].slice(1)) // [...arguments].slice(1) 获取传入的处第一个参数的所有实参,以数组格式
    return typeof res === 'object' ? res :obj
}
// 调用及测试
let lucy = newObject(CreatePerson, 'Lucy', '女')
console.log(lucy.name) // 'Lucy'
console.log(lucy.gender) // '女'
lucy.show() // 'hello,my name is Lucy'
lucy.motto() // 'study hard and make progress every day'

  好了,搞定!!

相关文章

  • JavaScript学习笔记3—面向对象编程

    JavaScript—面向对象编程 JavaScript不区分类和实例的概念,而是通过原型(prototype)来...

  • 面向对象编程

    面向对象的理解 JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。这...

  • Lesson-7 JS原型

    JavaScript 语言是通过一种叫做 原型(prototype)的方式来实现面向对象编程的.对象(object...

  • 面向对象编程

    JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。 xiaoMing...

  • 对象的封装(面向对象)&& 原型(prototype)

    JavaScript不区分类和实例的概念,是通过原型(prototype)来实现面向对象编程 类:类是对象的类型模...

  • JavaScript 面向对象编程-原型prototype

      上一篇谈到了四种创建对象的方式: 调用构造函数Object、以字面量的形式、工厂函数、构造函数。上一篇:Jav...

  • ajax

    1. 面向对象 javascript 具有面向过程,面向对象,函数式编程的特点 javascript 重要 原型/...

  • javaScript:3天理解面向对象(2)

    javaScript:3天理解面向对象(2) prototype 理解:每个构造函数都有prototype原型属性...

  • 原型、构造函数与面向对象编程

    1. 复杂的原型链 原型是 JavaScript 向面向对象编程语言进化的产物。 为什么要面向对象编程?为了代码复...

  • 2018-11-22

    JavaScript的面向对象是基于constructor(构造函数)与prototype(原型链)的。 构造函数...

网友评论

      本文标题:JavaScript 面向对象编程-原型prototype

      本文链接:https://www.haomeiwen.com/subject/tatthktx.html