javascript 对象原型记录(红宝书)

283 阅读6分钟
function Person(name, age,job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    }
}

var person1 = new Person("小明", 29, "工程师")
var person2 = new Person("小红", 20, "测试工程师")

new 操作叫符,经历了4个过程。

  1. 创建一个新的对象
  2. 将构造函数的作用域赋值给新的对象(因此this 就指向了这个对象)
  3. 指向这个构造函数中的代码
  4. 返回了新的对象 person1 和 person2 分别保存着一个 Person 的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向 Person,如下所示:
console.log(person1.constructor === Person) // true
console.log(person2.constructor === Person) // true

对象的 constructor 属性最初是用来标识对象类型的。但是,提到检测对象类型的话,还是instanceof 更可靠些。我们在这个例子中创建的对象,即是 Object 实例,也是 Person 实例。因为所有的对象都继承自 Object。

以这种方式定义的构造函数是定义在 Global 对象(在浏览中是window 对象)中的。 下面是在浏览器的控制台做的测试。

企业微信截图_76a38381-c25e-464b-bc60-6b5a8ddf5ea7.png

1、将构造函数当做函数

构造函数与其他函数的唯一区别就是调用它们的方式不同。任何函数,只要通过new操作符来调用,那它就可以作为构造函数,如果不通过new操作符来调用,就跟普通函数一样。例如: 定义的Person 可以通过下面这种方式调用。

// 当做构造函数使用
var person1 = new Person("小明", 29, "工程师");
person1.sayName(); // "小明"

// 作为普通函数使用
Person("小红", 20, "测试工程师");
window.sayName(); // "小红"

// 在另一个对象的作用域中使用
var o = new Object();
Person.call(0, "小红", 20, "测试工程师");
o.sayName(); // "小红"

不适用new操作符调用Person(),属性和方法都会被添加到window对象了。 这是因为,在全局作用域中调用一个函数是,this 的指向总是 Global(浏览器是window对象)。因此,在调用完函数之后,可以通过window对象来调用sayName()方法。

2、构造函数的问题

构造函数的主要问题,就是每个方法都要在每个实例上创建一遍。在当前的例子中, person1 和person2 都有一个名为 sayName() 的方法,但是两个方法不是同一个Function 的实例。ECMAScript 中的函数是对象,因此每定义一个函数,就是实例化了一个对象。 从逻辑的角度讲,此时的构造函数也可以这样定义:

function Person(name, age,job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("console.log(this.name)") // 与声明函数在逻辑上是等价的
}

因此,不同实例上的同名函数是不相等的。然而,创建两个完成同样任务的Function 实例的确是没有必要的。况且有this 对象在,根本不用再执行代码之前就把函数绑定在特定的对象上。因此,可以通过把函数定义转义到构造函数外部来解决这个问题。

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

function sayName() {
    console.log(this.name);
}

我们将sayName函数转移到构造函数外面,相当于是一个全局的函数。 person1 和 person2 共享了全局作用域中的同一个函数。这样确实解决了两个函数做同一件事情的问题。但是,全局作用域中的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更让人无法接受的是,如果我们需要定义很多的方法,那么就有很多个全局函数,我们自定义的引用类型就丝毫没有封装性可言了。

原型模式

function Person() {
    Person.prototype.name = "小明";
    Person.prototype.age = 29;
    Person.prototype.job = "工程师";
    Person.prototype.sayName = function() {
        console.log(this.name)
    }
}

var person1 = new Person();
person1.sayName(); // 小明

var person2 = new Person();
person2.sayName(); // 小明

console.log(person1.sayName === person2.sayName); //true

1、理解原型对象

创建一个新的函数,就会为这个函数创建一个 prototype 的属性,这个属性指向函数的原型对象。在默认情况下,所有的原型对象会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在的函数的指针。比如 Person.prototype.constructor 指向 Person。通过这个构造函数,我们还可以继续为原型对象添加其他的属性和方法。

创建了自定义构造函数后,其原型对象默认只会取得constructor属性。至于其他的方法,都是从Object继承来的。调用构造函数创建一个新实例后,该实例包含一个[[Prototype]]。真正的实现是在每个对象上支持一个属性__proto__。这个连接存在于实例于构造函数的原型对象之间,而不是实例于构造函数之间。下面是红宝书的图。

企业微信截图_dc226afa-0425-43a9-b9a4-c227bd2757e0.png

虽然我们的实例不包含属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象的属性的过程来实现的。

虽然所有的实现都无法访问[[Prototype]],可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。

console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true

console.log(Object.getPrototypeOf(person1) === Person.prototype) //true
console.log(Object.getPrototypeOf(person1).name) //小明

当我们读取某个属性的时候,会执行一次搜索。搜索从对象实例本身开始。如果在该实例中找到了具有给定名字的属性,则返回。没有的话,则继续搜索指针指向的原型对象。

虽然可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值,如果存在于原型实例中一个同属性名称,我们就在该实例中创建该属性,该属性会屏蔽原型中的值。

2、原型与in操作符

有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用时,in操作符会通过对象是否能够访问属性时返回true,无论属性在对象中还是原型中。hasOwnPrototype只有在属性存在于对象中才会返回truehasPrototypeProperty()只有属性在原型中才会返回true

3、更简单的原型写法

 function Person {
 }
 
 Person.prototype = {
     name: "小明",
     age: 29,
     job: "工程师",
     sayName: function() {
         console.log(this.name);
     }
 }

注意:,这里的constructor 属性不在指向Person了。我们这里的写法,本质上是完全重写的prototype对象,因此constructor属性也变成了新的对象的constructor属性(指向了Object构造函数),不在执行Person 构造函数。可以特意将constructor设置为特定的值。

 function Person {
 }
 
 Person.prototype = {
     constructor: Person,
     name: "小明",
     age: 29,
     job: "工程师",
     sayName: function() {
         console.log(this.name);
     }
 }

注意:,我们使用这种方式设置的constructor属性会导致它的[[Enumerable]]特性被设置为true,默认情况下,原生的constructor属性是不可枚举的。想要设置不可以枚举的话:

 function Person {
 }
 
 Person.prototype = {
     name: "小明",
     age: 29,
     job: "工程师",
     sayName: function() {
         console.log(this.name);
     }
 }
 //重设构造函数,只使用与ECMAScript5 兼容的浏览器
 Object.defineProperty(Person.pertotype, "constructor", {
     enumerablea: false,
     value: Person
 })
 

原型链