javascript面向对象与原型

60 篇文章 0 订阅
55 篇文章 1 订阅

1.原型

2.使用函数作为构造器

3.使用原型扩展对象

4.避免常见问题

5.使用继承创建类

了解了在JavaScript中函数是第一型对象,闭包可以使函数变得更加灵活、有用,还可以结合生成器函数与promise解决异步代码问题。现在要学习JavaScript中另一个重要的方面:原型。

可以在原型对象上增加特定属性。原型是定义属性和功能的一种便捷方式,对象可以访问原型上的属性和功能。原型类似经典的面向对象中的类(class)。实际上,JavaScript中原型的主要用途是使用类风格的面向对象和继承的方式进行编码,这与传统的基于类的语言的Java、C#类似,但也不完全是这样。

需要思考的问题:

原型的工作原理,探讨原型与构造函数的关系,如何模拟传统面向对象语言中的面向对象特性。关键字class,这是JavaScript的新特性,关键字class没有带来类的全部特性,但是已经很容易模拟类与继承了。

如何知道一个对象是否可以访问特定的属性?

在JavaScript中使用对象时,为什么原型链至关重要?

ES6中的关键字class是否改变了JavaScript中对象的工作机制?

在JavaScript中,对象是属性名和属性值的集合。

let obj = {

prop1:1, //简单赋值

prop2: function () {

 

}, //函数赋值

prop3:{} //对象赋值

}

 

//对象属性可以是简单值(如数值、字符串)、函数或者其他对象。同时,JavaScript是动态语言,可以修改或删除对象的属性。

//porp1保存一个简单的数字

obj.prop1 = 1;

//为对象属性赋值完全不同类型的值,这里是数组赋值

obj.port1 = [];

//从对象删除属性

delete obj.prop2;

//也可以为对象添加新属性:

obj.prop4 = "Hello";//添加一个全新的属性

console.log("The obj now is:" + JSON.stringify(obj));

 

在软件开发过程中,为了避免重复造轮子,我们希望可以尽可能的复用代码。继承是代码复用的一种方式,继承有助于合理地组织程序代码,将一个对象的属性扩展到另一个对象上。在JavaScript中,可以通过原型实现继承。

原型的概念很简单。每个对象都含有原型的引用,当查找属性时,若对象本身不具有该属性,则会查找原型上是否有该属性。

 

console.log("----------------------对象可以通过原型访问其他对象的属性-------------------");
//创建3个带有属性的对象
const yoshi = {skulk: true};
const hattori = {sneak: true};
const kuma = {creep:true};

//yoshi对象只能访问自身的属性skulk
if ("skulk" in yoshi) {
  console.log("Yoshi can skulk");
}

if (!('sneak' in yoshi)) {
  console.log("Yoshi cannot sneak");
}
if (!('creep' in yoshi)) {
  console.log("Yoshi cannot creep");
}

//Object.setPrototypeOf()方法,将对象hattori设置为yoshi对象的原型
Object.setPrototypeOf(yoshi, hattori);

//通过将hattori对象设置为yoshi对象的原型,现在yoshi可以访问hattori对象的属性
if ("sneak" in yoshi) {
  console.log("Yoshi can now sneak");
}
//目前hattori对象还不具有属性creep
if (!("creep" in hattori)) {
  console.log("hattori cannot creep");
}

//将kuma对象设置为hattori对象的原型
Object.setPrototypeOf(hattori, kuma);
//现在hattori对象可以访问属性creep
if ("creep" in hattori) {
  console.log("Hattori can now creep");
}
//通过将hattori对象设置为yoshi对象的原型,现在yoshi对象也可以访问属性creep。
if ("creep" in yoshi) {
  console.log("Yoshi can also creep");
}

 

 

 

在上面的例子中,我们首先创建三个对象:yoshi、hattori与kuma。每个对象具有独一无二的属性:只有对象yoshi具有属性skulk,只有对象hattori具有属性sneak,只有对象kuma具有属性creep。

最初每个对象只能访问自己具有的属性:

 

 

 

可以使用in操作符测试对象中是否具有某一特点的属性。

在JavaScript中,对象的原型属性是内置属性(使用标记[[prototype]]),无法直接访问。相反,内置的方法Object.setPrototypeOf需要传入两个参数,并将第二个参数设置为第一个对象的原型。

例如:

Object.setPrototypeOf(yoshi, hattori);

将yoshi的原型设置为hattori。

因此当chaxunyoshi上没有的属性时,yoshi将查找过程委托给hattori对象,通过yoshi访问hattori的属性sneak。

 

 

 

当访问对象上不存在的属性时,将查询对象的原型。在这里,我们可以通过对象yoshi访问hattori的属性snaek,因为hattori是yoshi的原型。

对hattori与kuma也是类似的做法。通过使用方法Object.setPrototypeOf,将对象hattori的原型设置为对象kuma。当我们查询对象hattori上没有的属性时,hattori将搜索委托到kuma上。

 

 

需要特别强调的是,每个对象都可以有一个原型,每个对象的原型也可以拥有一个原型,以此类推,形成一个原型链。查找特定属性将会被委托在整个原型链上,只有当没有更多的原型可以进行查找时,才会停止查找。

对象构造器与原型

如何创建一个新的对象?

const warrior = {}

创建一个新的空对象后,我们可以通过赋值语句添加属性:

const warrior = {}

warrior.name = 'Saito';

warrior.occupation = 'marksman';

JavaScript提供了这种机制,将对象的属性和方法整合为一个类,但与大多数语言有所不同。像面向对象的语言,如Java和C++,JavaScript使用new操作符,通过构造函数初始化新对象,但是没有真正的类定义。通过操作符new,应用于构造函数之前,创建创建一个新对象的分配。

console.log("-----------通过原型方法创建新的实例-----------");
//定义一个空函数,什么也不做,也没有返回值
function NinjaTest() {

}
//每个函数都有可置的原型对象,我们可以对其自由更改。
NinjaTest.prototype.swingSword = function () {
  return true;
};

//作为函数调用Ninja。验证该函数没有任何返回值。
const ninja1Test = NinjaTest();
if (ninja1Test === undefined) {
  console.log("No instance of Ninja created.");
}

//作为构造函数调用Ninja,验证不仅创建了新的实例,并且该实例上具有原型上的方法。
const ninja2Test = new NinjaTest();
if (ninja2Test && ninja2Test.swingSword && ninja2Test.swingSword()) {
  console.log("Instance exists and method is callable.");
}

 

 

 

 

上述代码中,我们定义了一个Ninja的空函数,并且通过两种方式进行调用:1.一种是作为普通函数调用, const ninja1Test = NinjaTest();另一种是作为构造器进行调用, const ninja2Test = new NinjaTest();

 

当函数创建完成后,立即就获得一个原型对象,我们可以对该原型对象进行扩展。在本例中,我们在在原型对象上添加了swingSword方法。

NinjaTest.prototype.swingSword = function () {

return true;

};

 

然后对函数进行调用。首先,是作为普通函数进行调用,并将返回结果存储在变量ninja1中。查看函数你会发现,函数没有返回值,所以检测ninja1Test的值为undefined。作为一个简单函数,NinjaTest并没有起作用。

然后,我们通过new操作符调用该函数,此次是作为构造函数进行调用,这一次已经创建了新分配的对象,并将其设置为函数的上下文(可通过this关键字访问)。操作符new返回的结果是这个新对象的引用。然后测试ninja2Test是新创建的对象的引用,具有swingSword方法。

 

 

 

由上图可以发现:

1.每一个函数都具有一个原型对象。

2.每一个函数的原型都具有一个constructor属性,该属性指向函数本身。

3.constructor对象的原型设置为新创建的对象的原型。

我们创建的每一个函数都具有一个新的原型对象。最初的原型对象只有一个属性,即constructor属性。该属性指向函数本身。当我们将函数作为构造函数进行调用时(如new Ninja()),新构造出来的对象被设置为构造函数的原型的引用。

在上面的例子中,在NinjaTest.prototype上增加了swingSword方法,对象ninja2Test创建完成时,对象ninja2Test的原型被设置为NinjaTest的原型。因此,通过ninja2Test调用方法swingSword方法委托到NinjaTest的原型对象上。注意,所有通过构造器NinjaTest创建出来的对象都可以访问swingSword方法,这就是一种代码复用的方式。

注意了swingSword方法是NinjaTest的原型属性,而不是ninja1Test实例的属性。那么两者的区别是(实例属性与原型属性之间)?

实例属性

当函数作为构造函数时,通过操作符new进行调用,它的上下文被定义为新的对象实例。通过原型暴露属性,通过构造函数的参数进行初始化。

console.log("--------------------实例属性之初始化过程中的优先级-----------------------");
function NinjaTest2() {
  //创建布尔类型的实例变量,并初始化该变量的默认值为false。
  this.swung = false;
  //创建实例方法,该方法的返回值为实例变量swung取反
  this.swingSword = function () {
    return !this.swung;
  }
}

//定义一个与实例方法同名的原型方法,将会优先使用哪一个?
NinjaTest2.prototype.swingSword = function () {
  return this.swung;
};

//创建NinjaTest2的一个实例,并验证实例方法方法会重写与之同名的原型方法
const ninjaTest2 = new NinjaTest2();
if (ninjaTest2.swingSword()) {
  console.log("Called the instance method,not the prototype method.");
}

 

 

通过上述代码可以发现:

在构造函数内部,关键字this指向新创建的对象,所以在构造器中添加的属性是直接在新的ninjaTest2 上的。然后,通过ninjaTest2 访问SwingSword属性时,就不需要遍历原型链,立即可以找到并返回了在构造器内创建的属性。

如下图所示:

上图反映出如果实例可以查找属性,就不会查找原型。

这里还有一个非常有意思的副作用!如下图所示:

 

 

由上图所示,每一个实例分别获得了在构造器内创建的属性版本,但是它们都可以访问一个原型属性

由于每个NinjaTest2实例都有自己的属性版本,这些属性在构造器内创建,并且均可访问相同的原型属性。这一点对于所有实例对象都是固定的属性值是没有影响的(如swung)。但是在某些情况下对于对象方法来说可能是由问题的。

在本例中,有三个版本的swingSword 方法,都执行相同的逻辑。只创建几个对象影响不大,但是如果创建大量对象则需要注意。因为每个复制方法都是一样的,创建大量的无意义的复制文件,仅仅是占用内存而已。当然,一般来说JavaScript引擎可能会执行一些优化,但是不能依赖JavaScript引擎。从这个角度看,只在函数的原型上创建对象方法是很有意义的,这样我们可以使得同一个方法为所有的对象实例所共享。

注意:

闭包:在构造函数内部定义方法,使得我们可以模仿私有对象变量。如果我需要私有对象,在构造函数中指定方法是唯一的解决方案。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值