JS 中对象、类与面向对象编程(一)(创建对象的一步一步优化过程)

本文详细介绍了JavaScript中对象、类和面向对象编程的基础知识,包括对象的创建、继承的理解以及构造函数模式。通过实例展示了数据属性和访问器属性的区别,以及如何使用Object.defineProperty()进行细粒度的属性控制。此外,还探讨了工厂模式和构造函数模式在创建对象时的角色和优缺点,为后续的原型模式和类的概念奠定了基础。

JS 中对象、类与面向对象编程(一)

  该章节会详细介绍 JS 中对象、类、继承的相关知识。设计到原型与原型链等相关重要知识点。我从最原始的创建对象 ==> 用工厂模式==> 构造函数模式 ==> 原型模式,一步步改进,讲述底层的原理,后序部分会涉及到原型的相关概念,都会总结的很清楚,内容按照 JavaScript高级程序设计第四版相关章节知识总结。防止文章内容太长,将其拆分为几个部分,这是第一部分。后序部分查看

本章节的目标:

  • 理解对象
  • 理解对象创建过程
  • 理解继承
  • 理解类

  该部分介绍:从最原始的创建对象==>用工厂模式==>构造函数模式。其中涉及每种方式解决的问题,以及还存在的问题,一步一步优化。为后面的原型模式,类的相关知识打下底层基础。

  ECMA-262 中对象的定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数”。即对象是一组没有特定顺序的值。对象的每个属性和方法都有一个名字,而每个名字都映射到一个值。所以可以把对象想象成散列表:一组名值对,其中值可以是数据或者函数。

8.1、理解对象

  可通过创建一个 Object 的实例,然后再为其添加属性和方法的方式创建自定义对象,如下:

let person = new Object();
person.name = "andy";				// 添加属性
person.age = 29;					// 添加属性
person.job = "Software Engineer";	// 添加属性
person.sayName = function(){		// 添加方法
	alert(this.name);
}

  之后,对象字面量成为创建这种对象的首先模式。如下:

let person = {
	name: "andy",				// 添加属性
    age: 29,					// 添加属性
    job: "Software Engineer",	// 添加属性
    sayName: function(){		// 添加方法
    	alert(this.name);
    },
	sayAge(){					// 添加方法 省略冒号:和function关键字也可以
    	alert(this.age);
    }
};

  上述两种方式创建的属性和方法都带有一些特征值,JavaScript 通过这些特征值来定义他们的行为。可以理解为属性的属性,例如 name 属性能否修改,能否删除,能否通过 for-in 循环返回属性等行为都是通过属性 name 的特征值来控制的。

8.1.1 属性的类型

  ECMAScript 中有两种属性:数据属性访问器属性

  • 数据属性

  数据属性是用来保存数据的,其中有4个描述其行为的特性,其中有一个特性[[Value]]是存放值的位置,这个位置可以读取和写入设置的属性的值。四个特性如下:

① [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中直接在对象上定义的属性,该特性的默认值都是 true。

② [[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中直接在对象上定义的属性,该特性的默认值都是 true。

③ [[Writable]]:表示能否修改属性的值。像前面例子中直接在对象上定义的属性,该特性的默认值都是 true。

④ [[Value]]包含这个属性的数据值。读取属性值的时候从这个位置读;写入属性的时候,把新值保存在这个位置。这个特性的默认值为 undefined。

  要修改属性的默认的特性,必须使用 ECMAScript5 中的 Object.defineProperty() 方法。这个方法接收三个参数:属性所在的对象属性的名字、和一个描述符对象。其中,描述符对象的属性必须是:configurable、enumerable、writable 和 value。设置其中一个或多个值。例如:

var person = {};
Object.defineProperty(person,"name",{
    writable: false,        // 设置"name"属性的 writable 特性值为 false,
    value: "andy"
});
alert(person.name);         // andy
person.name = "Grey";       // 尝试修改属性值,无效
alert(person.name);         // andy,不可修改的属性

  上述第7行代码在非严格模式下被忽略,在严格模式下报错。【注意】,①一旦使用 Object.defineProperty() 方法,如果没有指定特性的值,configurable、enumerable 和 writable 特性的默认值都是 false 【注意与在对象上定义属性时,特性的默认值不同】。②在 configurable 设置为 false 之前是可以多次调用 Object.defineProperty() 方法去设置同一个属性值的特性的,一旦 configurable 设置为 false 之后,就不允许在调用 Object.defineProperty() 方法去设置属性特性。严格模式下会报错。

"use strict";
let person = {};
Object.defineProperty(person,"name",{
    writable: false,        
    value: "andy"
});
// 下一行代码报错,因为第三行方法中为 configurable 特性设置了 false 值。
Object.defineProperty(person,"name",{
    value: "jack"
});

  数据属性其实就是更加底层的设置属性的方法,可以对属性进行更加细微的设置,可以完全用数据属性的方法为一个对象设置相关属性。

  • 访问器属性

  访问器属性不包含数据值。它们包含一个获取 (getter) 函数和一个设置 (setter) 函数,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性有 4 个特性描述它们的行为。

① [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。该特性的默认值都是 true。

② [[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中直接在对象上定义的属性,该特性的默认值都是 true。

③ [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。

④ [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。

  访问器属性是不能直接定义的,必须使用 Object.defineProperty()。下面是一个例子:

let book = {
	year_:2017,
	edition:1
};
Object.defineProperty(book,"year",{
	get() {
		return this.year_;
	},
	set(newValue){
		if(newValue > 2017){
			this.year_ = newValue;
			this.edition += newValue - 2017;
		}
	}
});
book.year = 2018;
console.log(book.edition);	// 2

  获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。

8.2、创建对象

  使用 Object 构造函数对象字面量可以方便地创建对象,但是创建具有同样接口的多个对象需要重复编写很多代码。能不能快速创建具有相同结构的多个对象呢?

let zhangsan = {
    name:"张三",
    age:18,
    sayName(){
        console.log(this.name);
    }
}
let lisi = {
    name:"李四",
    age:19,
    sayName(){
        console.log(this.name);
    }
}		 // 怎么快速创建多个相同类型的对象??? ==》类

8.2.1 概述

  ECMAScript 5.1 并没有正式支持面向对象的结构,比如类或继承。但是,巧妙地运用原型式继承可以很好的解决上述问题。

  ECMAScript 6 开始正式支持类和继承。不过 ES6 的类仅仅是封装了ES5.1构造函数加原型继承的语法糖而已。所以在学习 ES6 中的类之前,最好也要弄懂那些底层概念。

8.2.2 工厂模式

  工厂模式是一种设计模式,用于生产特定接口的对象,广泛应用于软件工程领域,用于抽象创建特定对象的过程。下面的例子展示了一种按照特定接口创建对象的方式:

function createPerson(name,age,job){    // 工厂函数
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(this.name);
    };
    return o;
}
let person1 = createPerson("张三",19,"student");
let person2 = createPerson("李四",25,"teacher");

  工厂模式可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

8.2.3 构造函数模式

  构造函数是用于创建特定类型对象的。例如 Object 和 Array 这样的原生构造函数,也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

function Person(name, age, job){	// 构造对象
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
}; }
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName();  // Nicholas
person2.sayName();  // Greg

  在这个例子中,Person() 构造函数代替了createPerson() 工厂函数。实际上,Person() 内部的代码跟 createPerson() 基本是一样的,工厂函数与构造函数区别如下。

  • 没有显式的创建对象。
  • 属性和方法直接复制给了 this。
  • 没有 return。

  另外,按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。

(1)在内存中创建一个新对象

(2)这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性

(3)构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)

(4)执行构造函数内部的代码(给新对象添加属性)。

(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

  实例对象中都有一个 constructor 属性,表示对象所属的类型,上述代码中的 person1 和 person2 都是由 new Person 产生的,所以属于 Person 类的对象,所以 constructor 属性指向 Person。不指向其祖先类型 Object 。

console.log(person1.constructor == Person);  // true
console.log(person2.constructor == Person);  // true
console.log(person1.constructor == Object);  // false
console.log(person2.constructor == Object);  // false

  constructor 本来是用于标识对象类型的。但 instanceof 操作符是确定对象类型更可靠的方式。前面两个对象都是 Object 的实例(因为所有自定义对象都继承自 Object ),同时也是 Person 的实例,所以调用 instanceof 操作符的结果如下:

console.log(person1 instanceof Object);  // true
console.log(person1 instanceof Person);  // true
console.log(person2 instanceof Object);  // true
console.log(person2 instanceof Person);  // true

  自定义构造函数可以创建多个类似对象,确保实例被标识为特定类型,相比于工厂模式,又进步了一点。

  构造函数可以写成上述函数声明的形式,写成函数表达式也可:

let Person = function(name, age, job) {		// 函数表达式
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
}; }
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName();  // Nicholas
person2.sayName();  // Greg

  在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。 只要有 new 操作符,就可以调用相应的构造函数:

function Person() {
  this.name = "Jake";
  this.sayName = function() {
    console.log(this.name);
  };
}
let person1 = new Person();
let person2 = new Person;
person1.sayName();  // Jake
person2.sayName();  // Jake
01. 构造函数也是函数

  构造函数与普通函数唯一的区别就是调用方式不同。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name);
  }; 
}
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); // "Nicholas"
// 作为函数调用
Person("Greg", 27, "Doctor"); 		// 函数调用,添加到 window 对象 
window.sayName(); 					// "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse"); 
o.sayName(); 		// "Kristen"

  这里要记住,在调用一个函数而没有明确设置 this 值的情况下 (即没有作为对象的方法调用,或者没有使用 call()、apply() 调用),this 始终指向 Global 对象(在浏览器中就是 window 对象)。

02. 构造函数的问题

  前面说到构造函数可以创建多个类似对象,并确保实例被标识为特定类型,但其定义的方法会在每个实例上都创建一遍。每 new 一个对象,就会去执行构造函数里的代码,当执行到构造函数的方法(本质也是一个函数)时,会新建一个函数对象,因此,每个 Person 实例都会有 自己的 Function 实例用于显示 name 属性。不同实例上的函数虽然同名却不相等,如下所示:

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = new Function("console.log(this.name)"); // 逻辑等价 
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName == person2.sayName); 	// false

  因为都是做一样的事,所以没必要定义两个不同的 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);
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName();  // Nicholas
person2.sayName();  // Greg

  在这里,sayName() 被定义在了构造函数外部。在构造函数内部,sayName 属性等于全局sayName() 函数。sayName 属性保存一个指向外部函数的指针。所以 person1 和 person2 共享了定义在全局作用域上的 sayName() 函数。

  这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,属于对象的方法暴露在全局作用域内,不止对象可以调用,在全局也可以调用。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。

  在进入到原型模式前,总结一下前面的改进过程,如下图:

属性描述

其他部分链接
JS 中对象、类与面向对象编程(二)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ItDaChuang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值