JavaScript 通过创建对象来理解对象

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kaw19950302/article/details/88846633

网上关于创建对象的博文已经烂大街了,写的比我好的也已经烂大街了,那我为什么还要写这篇博文呢,因为我发现有些东西写下来、敲出来才是真的理解了。至少这篇文章对我来说意义重大。

搜了下网上的创建对象的几种方法:有条回答获赞数比较高而且十分简洁:熟读 《JavaScript 高级程序设计》第六章。

简单粗暴,我就直接去翻尘封已久的犀牛书。

本文参考《JavaScript 高级程序设计(第三版)》,大部分代码 Copy,Copy 是抄写,不是复制粘贴;部分文字带有自己的理解。

先总结下读完这一章后引出的,我认为比较重要的知识点:

  • 对象的属性类型:数据属性和访问器属性
  • new 方法做了什么
  • 基本数据类型和引用数据类型
  • 栈内存与堆内存

——————————————————————————————————————————————————

正文:

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

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

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

创建对象的方法

  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

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

在这里是图片描述

无论什么时候:

  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"——来自原型

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

在这里插入图片描述

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

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 方法做了什么
  • 基本数据类型和引用数据类型
  • 栈内存与堆内存

以上是我在读第 6 章第一节和第二节后的笔记以及收获,有什么不对的地方还麻烦大家提出,互相讨论,感激。

猜你喜欢

转载自blog.csdn.net/kaw19950302/article/details/88846633
今日推荐