1. 对象的定义

面向对象的语言都有一个标志,即类。

定义:对象是 JavaScript 的一个基本数据类型,是一种复合值,它将很多值(原始值或者其他对象)聚合在一起,可通过名字访问这些值。即属性的无序集合

例:上帝根据自己的形象造男造女,这里的上帝便是,这里的 男和女 便是对象

在对象中,每一个属性和方法都已一个名字,而每个名字都映射到一个值,即 ECMAScript 中的对象无非就是一组名值对,其中值可以是数据或者函数。

2. 对象的属性

在 JavaScript 中,对象的属性分为两种类型:数据属性和访问器属性。

2.1 数据属性

1. 数据属性:包含的是一个数据值的位置,在这可以对数据值进行读写。
2. 数据属性包含四个特性,分别是:
属性 含义
configurable 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为 true
enumerable 表示能否通过 for-in 循环返回属性
writable 表示能否修改属性的值
value 包含该属性的数据值。默认为 undefined

var App = {
    name: '其他',
}
Object.defineProperty(App,'name',{
    writable: false, // 属性不可修改 
    configurable: false,
    value: '大树云',
})
console.log(person) // 大树云

2.2 访问器属性

1. 访问器属性:这个属性不包含数据值,包含的是一对 get 和 set 方法,在读写访问器属性时,就是通过这两个方法来进行操作处理的。
2. 访问器属性包含四个特性,分别是:
属性 含义
configurable 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为 false
enumerable 表示能否通过 for-in 循环返回属性,默认为false
get 在读取属性时调用的函数,默认值为 undefined
set 在写入属性时调用的函数,默认值为 undefined

var App = {
    _year: 2019, // 内部属性,只能通过对象的方法来读写
    edition: 1
}
Object.defineProperty(App,'year',{
    get: function(){
        return this._year
    },
    set: function(newValue){
        if(newValue > 2004){
            this._year = newValue;
            this.edition += newValue - 2019;
        }
    }
})
App.year = 2020
console.log(App.edition) // 2 修改后的值
console.log(App.year) // 2020 修改后的值

3. 创建对象的方法

  1. 最简单粗暴:对象字面量
var person = new Object();
persion.name = 'Bob';
persion.age = 24;
persion.job = 'Software Engineer'
persion.sayName = function(){
    consolo.log(this.name)
}
var person = {
    name: 'Bob',
    age: 24,
    job: 'Software Engineer',
    sayName: function(){
        consolo.log(this.name)
    }
}
  1. 工厂模式
function createDevice(name,userId,job){
    var obj = new Object();
    obj.name = name;
    obj.userId = userId;
    obj.job = job
    obj.sayName = function(){
        console.log(this.name)
        return this.name
    }
    return obj
}
var device1 = createDevice('device1',1,'light')
console.log(device1) // { name: 'device1', userId: 1, job: 'light', sayName: [Function] }
console.log(device1 instanceof Object) // true
  1. 构造函数模式
function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
}; }
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1) // Person {name: 'Nicholas',age: 29,job: 'Software Engineer',sayName: [Function] }
console.log(person1 instanceof Object) // true,

创建 Person 实例,必须使用new 操作符,这种方式实际上经历了以下四个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

缺点: 每个方法都要在每个实例上重新创建一遍,所有实例内部的 Function 都不是一个同一个 Function,(注: 因为在 JS 中,一切皆对象,联想到函数声明,每定义一个函数也就是实例化一个对象,逻辑角度是等价的):

console.log(person1.sayName === person2.sayName) // false
  1. 原型模式
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
person1.sayName();   //"Nicholas"
var person2 = new Person();
console.log(person1.sayName === person2.sayName) // true

这样就不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象中

20190327154213320

无论什么时候:

  1. 只要创建了函数,就会根据函数特定的规划为函数创建一个 prototype 属性,这个属性指向函数的原型对象
  2. 原型对象自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针

但是,为什么说这样的方式可以避免出现像构造函数那样的问题呢?

原来是这样:在调用 person1.sayName() 的时候,解析器首先会问:

  1. “实例 person1sayName 属性吗?” 答:“没有。"
  2. 然后,它继续搜索,再问:“person1 的原型有 sayName 属性吗? ”答:“有。”
  3. 于是,它就读取那个保存在原型对象中的函数。当我们调用 person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理

JS 对象的理解好像又深刻了,尝试以下代码:

console.log(Person.prototype) 
// Person {name: 'Nicholas',age: 29,job: 'Software Engineer',sayName: [Function] }
console.log(person1.prototype) 
// undefined
console.log(person1.constructor.prototype) 
// Person {name: 'Nicholas',age: 29,job: 'Software Engineer',sayName: [Function] }

所以:有人说,对Person.prototype 的理解是:它是函数的原型对象,现在看来,这种理解是不够准确的。

其实:

  • prototype 只是每个函数在被创建时自带的一个属性,这个属性指向函数的原型对象,
  • 从而可以使用person1.constructor 获取原型对象中的 constructor 属性,
  • construtor 指向的是 Person
  • 所以 person1.constructor.prototype 也不难理解

(好吧,刚开始真的有点绕,多读几次其实还好)

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这
个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性,举例如下
person1.name = "Greg";
alert(person1.name); // "Greg"——来自实例
alert(person2.name); // "Nicholas"——来自原型

delete person1.name;
alert(person1.name); // "Nicholas"——来自原型

// 注意理解是屏蔽,我的理解也就是优先级是与作用域保持一致,但是只是屏蔽,并没有改变原型中属性的的值。

20190327154252427

有个方法可以判断一个属性是存在于实例中,还是存在于原型中。

person1.name = "Greg";
alert(person1.name); // "Greg"——来自实例 
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); // "Nicholas"——来自原型 
alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); // "Nicholas"——来自原型 
alert(person1.hasOwnProperty("name")); //false
  1. 原型模式进阶
function Person(){}
Person.prototype = {
    name : " Nicholas ",
    age : 29,
    friends : ["Shelby", "Court"],
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

原型模式的缺点:

如果原型对象上面的属性所对应的值是引用类型,那么问题就来了。众所周知,对于 JS的数据类型分为两类:基本类型 引用类型,我所理解的他们的区别主要是存储的空间不同:

  • 基本数据类型存在于栈内存,键值对的方式存储
  • 对于引用数据类型,栈内存中存的是键和引用地址,而引用地址指向的是堆内存中该对象所存储的地方

所以:

person1.friends.push("Ayden");
alert(person1.friends);    //"Shelby,Court,Ayden"
alert(person2.friends);    //"Shelby,Court,Ayden"
alert(person1.friends === person2.friends);  //true

因为它们的引用地址指向的是同一个堆内存对象(数组),所以,每个实例一般都是有专属于自己的属性的。

书中是这样说的:

假如我们的初衷就是像这样:
在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部
属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。

  1. 组合使用构造函数模式和原型模式
function Person(name, age, job){
this.name = name; 3 this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
 2
  }
Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Ayden");
alert(person1.friends);    // "Shelby,Count,Ayden"
alert(person2.friends);    // "Shelby,Count"
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true

实例所有的属性都用构造函数定义,所有的方法以及 constructor 属性都早原型对象中定义。

都这样做的好处就是确保每个实例的属性都是自己独立的,但是共享了对方法的引用,最大限度的节省了内存空间。

  1. 动态原型模式
function Person(name, age, job){
    //属性
    this.name = name; 
    this.age = age; 
    this.job = job;
    //方法
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        }; 
    }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
困惑解决:

学习了组合与动态原型模式之后,突然有个问题困惑了,直接上代码:

// 代码 1
function Person(name, age, job){
    this.name = name; 
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
    Person.prototype = {
        constructor : Person,
        sayName : function(){
            alert(this.name);
        }
    } 
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName === person2.sayName) // false


// 代码 2
function Person(name, age, job){
    this.name = name; 
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
} 
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName === person2.sayName) // true

论封装性,代码一不是更好吗?为什么都是 prototype 属性的重写,代码一和代码二的结果却不一样?

我想到了 new 关键字做了什么:创建对象,指针指向,执行函数,返回对象,我应该是忽略了执行函数这一步。

代码一中,每次实例化对象,prototype 属性都进行了重写,重写了两次,所以改变了现有实例与新原型之间的联系。

而代码二中,虽然同样是两次实例化,但不同的是全局代码也就是给 Person 的原型定义只执行了一次,所以 sayName 方法的引用指向的是同一个堆内存对象。

书中是这样写的:

使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

所以,之所有会有以上困惑,是因为对 new 关键字做了什么把握不清。

还有两种创建对象的模式,寄生构造函数模式稳妥构造函数模式,大树云项目中目前还没有用到过,这里就不做赘述。

本文代码片段参考《JavaScript 高级程序设计(第三版)》第六章

了解以下知识点或许对本文的理解更有帮助

  • JavaScript new 方法做了什么
  • JavaScript 基本数据类型和引用数据类型
  • 栈内存与堆内存

Ayden
85 声望2 粉丝

I'd rather be undefined than null.