文章

《Head First JavaScript》读后感及原型相关概念的理解

JavaScript
读后感

《Head First JavaScript》读后感及原型相关概念的理解

《Head First JavaScript》读后感及原型相关概念的理解

首先来谈一谈读后感

这本书大约有 650 多页,刨去其中的练习及其答案页,也应该有 500 多页,所以你可以想想里面有着多少关于 JavaScript 的知识。有人可能看到这么厚的书,就已经退避三舍、望而却步了。其实,不用害怕,如果你拿这本书来入门 JavaScript,我觉得非常好。其实按道理来说,不管哪本书,你只要认真耐着性子去看完读完,相信都会有一番收获的。当然关于这本书,最让我觉得异于其他编程语言书的是,里面充斥着有趣的图画以及作者幽默风趣的描述。给我的感觉是,当遇到一个比较难理解的概念时,作者不会像那些古板的人一样,直接抛出概念让你自己去理解,而是以朋友的角度来和你面对面说一样。因而,我读完这本书,里面的任何概念我都能够理清,并且在其他任何代码中看到它时,我都能认出它。所以,这本书总的来说就是风趣幽默的同时又给你认真地讲解概念,(也可能是由于国外作者的缘故,让这本书形成了这种叙述风格),好书当然得重读一遍,不过价格确实有点小贵,单本 129 元,不过我当时在某东遇上活动买了一堆书,就显得这本书单价就很便宜了,值得推荐。


对象模型

由于我之前学过 Java,就在学 JavaScript 之前了解过一点对象的概念,但这并不妨碍我去理解 JS 中的对象,反而是一种帮助。学过 Java 的都知道,创建对象需要通过类,但 JavaScript 根本就没有类,而是通过从其他对象那里继承行为和属性,这被称为原型式继承,也叫基于原型的继承。


在没有学习过原型继承之前,我们都是通过对象字面量来创建一个对象,比如:

var fido = {
  name: "Fido",
  breed: "Mixed",
  age: 5,
};

通过对象字面量创建对象,则当需要创建多个对象时,你需要在对象里面,为它添加很多属性和方法,而且某些属性和之前完全相同,你只是在不停的重用它,而且代码量大大增加。所以,还有一种创建对象的方法,是通过对象构造函数来创建的。它能帮助我们创建大量的对象,且创建不同对象时,不需要重用其属性,例如:

function Dog(name, breed, age) {
  this.name = name;
  this.breed = breed;
  this.age = age;
}
var fido = new Dog("Fido", "Mixed", 5);
var spot = new Dog("Spot", "Chihuahua", 10);

在上面的代码中,我们定义了一个叫 Dog 的构造函数,它里面包含 name,breed,age 三个属性。因此我们创建不同的小狗对象时,只需要为每个不同的小狗对象传入不同的实参即可。


那么,构造函数的工作原理又是什么?

说说其工作原理

通过观看上面的代码,我们可以看到当创建对象时,都要写一个 new 运算符来调用构造函数。那么 new 运算符都做了什么呢?我们的视线都暂且停留在赋值运算的右边:1、new 运算符创建了一个新的空对象。2、new 设置 this,使其指向刚刚创建的新对象。3、调用构造函数 Dog,将三个实参传递给 Dog 的三个形参。4、构造函数 Dog 给新创建的 this 指向的对象的属性赋值。5、Dog 函数执行完后,new 返回 this,指向新创建的对象的引用。这里需要注意的是,它会为你自动地返回 this,你无需在代码中显示地返回。返回 this 后,我们将其赋值给变量,存储在变量中。

能为对象添加属性和方法,在构造函数中也一样能干。

对象在JS中是一个动态的结构,无论对象包含哪些属性和方法,其类型都是 Object当用 new 运算符调用一个构造函数时,都将创建一个新的对象实例。由同一个构造函数创建的对象都是相同类型的对象。


当我们用构造函数创建对象时,如果出现了包含的形参过多怎么办?

针对这种情况,我们就要将对象字面量与对象构造函数相结合使用了,显然极端地使用某一种方式在某些问题上,根本行不通。例如:

function Car(make,model,year,color,passengers,convertible,mileage){
               ...
}
var cadi = new Car("GM","Cadillac",1955,"red",5,false,12892);

上面的例子能看出,使用构造函数创建对象时,需要传入的实参实在是过多了。如何解决这种问题?我们将所有实参放到一个对象字面量中,再将这个对象字面量传给函数。这就像通过一个容器来传递所有的值。例如:

var cadiparam = {
  make: "GM",
  model: "Cadillac",
  year: 1955,
  color: "red",
  passengers: 5,
  convertible: false,
  mileage: 12892,
};
var cadi = new Car(cadiparam);

好像对于代码量来说,好像并没有啥改变。但对于在给构造函数传入实参时,能够避免出现不必要的错误。


好了,扯了那么久,又回到原型了

当我们用上述的构造函数创建对象时,每个对象都有自己的副本,当在其中添加方法时,由于对象都有其副本,所以在运行阶段时,每个对象都将创建一组新的方法这会占用计算机的资源,并且影响应用程序的性能

我们可以通过扩展其他对象来创建对象,也就是通过原型对象来创建对象由原型对象创建而成的对象,它继承了原型对象的属性和方法,我们在这里可以将原型对象想象成父类,将由原型对象创建而成的对象想象成子类,那么子类就自然而然地继承了父类的所有方法及属性。在继承了原型的对象中,只需加入新的属性和方法,原型有的属性和方法不用重写,因为它继承了这些属性和方法。


继承的工作原理又是什么?

当对象实例调用了自己本身没有的属性和方法时,JavaScript 如果在对象中找不到,那么将在原型中查找它。也就是说,查找的顺序一定是:先在对象实例中查找——>对象实例中找不到,就向继承链上方查找,也就是在原型中查找。继承原型并不意味着必须与它相同,当然可以重写原型的属性和方法。

如何创建一个原型?

构造函数中有一个属性 prototype,该属性是一个指向原型的引用。

如何设置原型?

通过构造函数的一个属性 prototype 来访问原型对象。大致的顺序为:先创建一个构造函数,再获取其属性 prototype,再为原型添加属性和方法,依次来设置原型。例如:

function Dog(name, breed, weight) {
  this.name = name;
  this.breed = breed;
  this.weight = weight;
}
Dog.prototype.species = "Canine";
Dog.prototype.bark = function () {
  if (this.weight > 25) {
    console.log(this.name + " says woof!");
  } else {
    console.log(this.name + " says yip!");
  }
};

在上方代码中,我们为 Dog 原型中,新增了属性 species 和方法 bark。在任何情况下,this 都指向原始对象,即方法被调用的对象。原型是动态的,也就是说,当我们在原型中新增了一个属性和方法时,继承该原型的任何对象都能使用这个方法和属性。

如何判断使用的属性是包含在实例中还是原型中?

每个对象都有一个方法hasOwnProperty如果属性是在对象实例中定义的,则返回 true,如果是在原型中定义的,则返回 false。其参数为一个字符串

原型链

对象不仅可以继承一个原型的属性,还可以继承一个原型链。原型链中的继承原理,和上面说到的继承原理是一样的,先在对象实例中查找,找不到则沿着原型链向上查找

那么如何继承一个原型对象呢?

依然要用到 new 运算符,例如:

function Dog(name, breed, weight) {
  this.name = name;
  this.breed = breed;
  this.weight = weight;
}
function ShowDog(name, breed, weight) {
  this.name = name;
  this.breed = breed;
  this.weight = weight;
}
ShowDog.prototype = new Dog();

依据上方的代码 ,我们成功地创建了一个继承原型对象的对象,顺序依然是创建该对象的构造函数,通过函数名调用其属性 prototype,然后将其属性 prototype 设置为一个新的对象实例。新创建的原型对象,依然是原来原型的一个对象实例。新的原型创建成功后,我们就可以给它添加新的属性和方法了。

值得注意的是,所有对象都是从 Object 派生而来,它是对象的始祖,实际上 Object 也有原型,其原型为Object.prototype

结束语

《Head First JavaScript》这本书就此结束啦,在 JavaScript 中,一切几乎皆是对象,学习永远没有解脱的时候。