原型 原型链 构造函数 继承(上)

直接上代码,让我们慢慢理解原型(对象) 构造函数的关系

原型(对象)是什么

// 这里我们只是声明了一个叫做xiaolu的函数,没有对它进行
// 任何多余的操作,让我们看看它身上会有什么不同吧

function Xiaolu() {
    
}
console.log(Xiaolu)
console.log(Xiaolu.prototype)
console.log(Xiaolu.prototype.constructor)

在这里插入图片描述

我们来看看这仅仅只声明了函数的代码打印的结果
令人疑惑,明明没有给这个函数添加任何属性,但是为什么能打印出xiaolu.prototype呢
在上述代码中,虽然只是声明了一个函数,但浏览器却干了很多事情:
首先,js引擎编译时发现声明了一个叫Xiaolu的函数,然后告诉浏览器: “我这里有一个函数声明,请你在内存中创建一个对象,让我把声明的这个函数的prototype指向它。你也要记得把constructor指回来哦”
于是乎,浏览器创建了一个对象obj,把声明的函数的默认prototype属性指向了obj,并将obj默认属性constructor指回了声明的函数。
这个obj,就是声明函数的原型对象,也可以说是函数的原型。
在这里插入图片描述
上图就是函数声明时的属性指向
关于obj中的__proto__,马上就会提到。

构造函数是什么

很多oop类语言都有构造函数这一概念,它是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值(来自百度百科)
在Java中声明一个类,其构造函数必须与类同名,其构造函数是对象一建立就运行,给对象初始化,包括属性,执行方法中的语句。而js却有些不一样

class HelloWorld{        
    HelloWorld(){        //定义构造函数,输出Hello World
        System.out.println("Hellow World");
    }
}

在js中,我认为所有的函数都拥有称为构造函数的能力。即使再普通的函数,只要和new一起创建对象时,我就愿称它为构造函数。
当调用new 构造函数创建一个对象时,会发生以下4个步骤:

  1. 创建一个全新的对象
  2. 这个对象会被执行[[prototype]]连接
  3. 这个新对象会被绑定到函数调用的this(this指向之后我也会说)
  4. 如果函数没有返回对象,就返回新对象

即使是构造函数,也脱离不了是函数这个事实,因此它会有原型对象(参考第一节 原型),然后创建对象的第2个步骤就是执行[[prototype]]连接,所以构造函数创建的对象中的[[prototype]]会指向构造函数的原型对象,这个[[prototype]]属性没办法直接访问,但是chrome和火狐浏览器提供了对[[prototype]]属性的访问的方法:proto(都是两个下划线)

function Xiaolu() {
    
}
// 通过new Xiaolu这个函数变成了构造函数
// 并且成功创建了对象xiaolu
var xiaolu = new Xiaolu()
// chrome和火狐浏览器才支持,对[[prototype的访问]]
console.log(xiaolu.__proto__)

在这里插入图片描述

可以看出,proto,也就是被创建的对象中的[[prototype]]是指向构造函数的原型对象的
在这里插入图片描述

原型和构造函数

接下来,看段代码,来分析分析原型和构造函数
在这里插入图片描述

看不懂这段代码先不要急,来一行一行分析
第14行代码

// 在Xiaolu原型对象上添加属性
Xiaolu.prototype.text = '小鹿'

得出:可以手动给原型对象添加属性(方法也可),会立马生效。(由于在原型中查找值的过程是一次搜索,因此对原型对象所做的任何修改都会立即从创建的对象中反映出来,原因也是归结为创建的对象与原型之间的松散连接关系)

第17行代码 和 第23行代码

// 通过new Xiaolu构造函数创建 xiaolu1对象 
// xiaolu1中的[[prototype]]指向Xiaolu原型对象
var xiaolu1 = new Xiaolu()
// 通过new Xiaolu构造函数创建 xiaolu2对象 
// xiaolu2中的[[prototype]]指向Xiaolu原型对象
var xiaolu2 = new Xiaolu()

得出:构造函数可以创建多个对象,并且对象之后跟构造函数没有关系了,只是[[prototype]]指向构造函数的原型对象

第20行代码

// 访问xiaolu1的text属性
// 打印结果是 小鹿
console.log(xiaolu1.text)

得出:因为xiaolu1是没有text属性的,但还是打印出了"小鹿",我们也能看到“小鹿”是原型对象上text属性的值,由此可见,如果访问一个对象的属性,会先在当前对象中查找是否有这个属性,如果有就使用这个属性,如果没有,就会去[[prototype]]指向的原型对象中查找是否有这个属性,如果原型中还没找到,就会去原型的原型上找(这就是原型链,先不谈)

第26行代码 和 第29行代码

// 访问xiaolu2的text属性
// 打印结果是 小鹿
console.log(xiaolu2.text)
// 看看xiaolu1和xiaolu2两个对象的text属性是否相同
// 打印结果 true
console.log(xiaolu1.text === xiaolu2.text)

得出:同样,xiaolu2也是没有text属性的,所以会去[[prototype]]指向的原型对象中找

第30行以后代码

// 修改xiaolu1的text属性
xiaolu1.text = '小鹿1'
// 打印结果 小鹿1
console.log(xiaolu1.text)
// 打印结果 小鹿
console.log(xiaolu2.text)

得出:通过构造函数创建的对象并不能修改原型对象的属性,而是给自己创建了一个text属性
如果当前对象中有需要的属性,就不会去原型对象中查找,因此产生了遮蔽效果,在对象中创建原型对象中的同名属性,以此遮蔽原型对象的属性

构造函数和原型组合创建对象的缺点与改进

构造函数创建对象

构造函数创建对象时,会根据构造函数相应添加属性和方法,但是创建不同对象时,它们的属性和方法都不是同一份的,是不共享的。对于属性来说,不共享是我们想要的。但方法,我们却希望它能被共享,因为方法执行起来是一样的,每个对象都创建一个一样的方法,那样属实上头。
总结一下,构造函数创建对象的缺点:方法不共享!
优点:属性不共享

 // 给准备去充当构造函数的函数添加属性和方法
 function Xiaolu(name, age) {
     this.name = "小鹿"
     this.age = 18
     this.code = function() {
     console.log(this.name + '正在敲代码')
     }
     // 但是有个缺点,构造函数创造的多个对象,其中的
     // 属性和方法都是单独的,这对于属性来说,挺好的
     // 但是方法都是一样的,每个都添加一个一样的方法
     // 这是非常不友好的。
 }
 // 构造函数创建两个对象
 let xiaolu1 = new Xiaolu()
 let xiaolu2 = new Xiaolu()
 // 分析对象上的方法是否共用
 // 打印结果:false 不共用
 console.log(xiaolu1.code === xiaolu2.code)

在这里插入图片描述

原型创建对象

原型创建对象时,它省略了为构造函数传递初始化参数这一环节,结果所有对象都是取得相同的属性值。虽然有些不便,但还有个更大的问题。
对于基本类型来说,给对象的基本类型赋值会在对象上添加一个同名属性,产生遮蔽效果
但对于引用类型来说,例如给数组添加一组元素,这并不会在对象上添加同名属性,而是直接对原型对象上的数组进行操作,所以会改变所有创建出来的对象对这个数组的引用。
这就是原型创建对象的最大缺点:引用类型属性会共享
优点:方法共享

function Xiaolu() {
}
// 给原型对象上添加属性
// 基本类型
Xiaolu.prototype.name = '小鹿'
// 引用类型
Xiaolu.prototype.work = ['工作', "代码", "视频"]
let xiaolu1 = new Xiaolu()
let xiaolu2 = new Xiaolu()
// 给xiaolu1添加name属性
xiaolu1.name = '小鹿1'
// 触发遮蔽效果
// 打印结果:false
console.log(xiaolu1.name === xiaolu2.name)
// 修改xiaolu1的引用类型
xiaolu1.work.push('xiaolu1加的')
// 通过打印结果判断创建的多个对象是否共享引用类型
// 打印结果:true
console.log(xiaolu1.work === xiaolu2.work)

在这里插入图片描述

构造函数和原型组合创建对象

拿着这两个缺点,去思考,什么时候方法会共享,基本类型和引用类型属性都不共享?
不就是把两者的优点结合一下吗:构造函数创建对象不共享属性。原型创建对象共享方法
这个问题就可以解决了,通过给原型对象添加方法,给构造函数添加属性。这样创建出来的对象属性不共享,方法共享,ok,目的达成,上代码

// 给要去充当构造函数的函数添加属性
function Xiaolu(name, age) {
    this.name = name
    this.age = age
} 
// 给原型对象上添加方法
Xiaolu.prototype.code = function() {
    console.log(this.name + '在敲代码')
}
// 构造函数创建两个对象,获得属性
let xiaolu1 = new Xiaolu("小鹿1", 18)
let xiaolu2 = new Xiaolu("小鹿2", 10)
// 分析两个对象上的属性,方法是否共用
// 打印结果:false
//          true
console.log(xiaolu1.name === xiaolu2.name)
console.log(xiaolu1.code === xiaolu2.code)

在这里插入图片描述

动态原型创建对象

得出了一个似乎完美的方法,但方法写在构造函数外面的方式,始终不太习惯,所以试着把给原型对象添加方法写入到构造函数内部,这就引出了动态原型创建对象。
动态原型,字面意思:根据原型上方法是否存在,动态添加
结合代码理解

function Xiaolu(name, age) {
  this.name = name
  this.age = age
  // 会检测是否存在code方法
  // 如果存在就不会再创建,因此只会创建一次code方法
  // 所以叫做动态原型创建对象
  if(typeof this.code !== 'function') {
    // 给原型对象添加code方法,当然是在检测没有创建code方法之后
    Xiaolu.prototype.code = function() {
      console.log(this.name)
    }
  }
}
    
let xiaolu1 = new Xiaolu("xiaolu1", 18)
let xiaolu2 = new Xiaolu("xiaolu2", 10)
// 看看对象的属性和方法是否共用
// 打印结果:false
//        :true
console.log(xiaolu1.name === xiaolu2.name)
console.log(xiaolu1.code === xiaolu2.code)

在这里插入图片描述

这里把添加方法写入了构造函数内部,通过判断对应的code是否是一个函数(排除同名属性),给原型对象动态添加方法,这种写法完美解决了之前的几个缺点:
1、构造函数创建对象,方法不共享
2、原型和构造函数组合创建对象时,添加对象的代码写在了构造函数外部,不太符合封装的思想。

动态原型创建对象是js中用的最多的创建对象方法。也推荐大家使用这一方法
原型(对象)和构造函数大概就讲到这里
之后会给大家讲讲继承和原型链
当然还有一些基本属性和操作符,包括Object.create等

参考自:javascript高级程序设计 你不知道的javascript上卷

猜你喜欢

转载自blog.csdn.net/qq_46299172/article/details/107249779
今日推荐