对象,类与面向对象编程 上

目录

前言

理解对象

 属性的类型

数据属性【Data Properties】

访问器属性【Accessor Properties】

 合并对象

 对象标识及相等判定

增强的对象语法

1. 属性值简写

2.可计算属性

3.简写方法名

4.对象解构

5.嵌套解构

创建对象 

工厂模式

 构造函数模式

原型模式

原型是如何工作的

 原型层级 

原型和in操作符 

属性枚举顺序

对象迭代

 另一种原型语法

 原型的动态性

原生对象原型 

 原型的问题


前言

ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未讨论的原因),可以把ECMAScript 的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。

理解对象


创建自定义对象的通常方式是创建Object 的一个新实例,然后再给它添加属性和方法,如下例所示: sayName()方法会显示this.name 的值,这个属性会解析为person.name

let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
	console.log(this.name);
};

后来对象字面量的方式越来越受欢迎

let person = {
 name: "Nicholas",
 age: 29,
 job: "Software Engineer",
 sayName() {
  console.log(this.name);
 }
};

 属性的类型

ECMA-262 标准定义了一些只内部使用的属性,这些属性分为两类:数据属性和访问器属性。为了表明这些属性是内部使用,它们被双中括号包裹着。

数据属性【Data Properties】

  • [[Configurable]] 表明属性是否可以通过 delete 重新定义,默认为true
  • [[Enumerable]] 表明属性是否可以通过 for-in 循环返回,默认为true
  • [[Writable]] 表明是否可以更改属性值,默认为true
  • [[Value]] 包含属性的真正值,默认值是 undefined

当用对象字面量定义一个属性值时,[[Configurable]],[[Enumerable]], 和[[Writable]]都被设为 true 同时[[Value]]属性被设为给定的值,

修改属性的默认特性,就必须使用Object.defineProperty()方法。这个方法接收3 个参数:

  • 要给其添加属性的对象
  • 属性的名称
  • 一个描述符对象
    let person = {};
    Object.defineProperty(person, "name", {
        configurable: false,
        writable: false,

        value: "Nicholas"
    });
    console.log(person.name); // "Nicholas"
    person.name = "Greg";
    console.log(person.name); // "Nicholas" 
    delete person.name;
    console.log(person.name); // "Nicholas"
    // 无法重新定义属性:name
    Object.defineProperty(person, "name", {
        configurable: true,
        value: "Nicholas"
    });

设置 writable 为false之后企图改变属性的值没有成功,严格模式下则会抛错

设置 configurable 为false之后企图删除属性没有成功。

设置了 configurable 为false之后,不能再设置为 true,企图设置为 true一律报错

访问器属性【Accessor Properties】

  • [[Get]] 获取一个属性值,默认值是 undefined
  • [[Set]] 设置一个属性值,默认值是 undefined
    let book = {};
    Object.defineProperties(book, {
        year_: {
            value: 2017
        },

        edition: {
            value: 1
        },

        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

读取属性值

Object.getOwnPropertyDescriptor() 方法接收俩个参数:对象本身和属性名

 合并对象

ECMAScript 6 专门为合并对象提供了Object.assign()方法。这个方法接收一个目标对象和一个或多个源对象作为参数,

然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回true)和自有(Object.hasOwnProperty()返回true)

属性复制到目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。

let dest, src, result;
/**
* 简单复制
*/
dest = {};
src = { id: 'src' };
result = Object.assign(dest, src);
// Object.assign 修改目标对象
// 也会返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src }
console.log(dest); // { id: src }


因此,Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。

 对象标识及相等判定

在 ECMAScript6 之前,全等 === 符号不能判断某些情形

console.log(true === 1);  // false
console.log({} === {});   // false
console.log("2" === 2);   // false

// These have different representations in the JS engine and yet are treated as equal
console.log(+0 === -0);  // true
console.log(+0 === 0);   // true
console.log(-0 === 0);   // true

// To determine NaN equivalence, the profoundly annoying isNaN() is required
console.log(NaN === NaN); // false
console.log(isNaN(NaN));  // true

ES6 新增了 Object.is() 修复这些问题,如果要判断的对象大于2,则使用递归 

function recursivelyCheckEqual(x, …rest) {
 return Object.is(x, rest[0]) && 
     (rest.length < 2 || recursivelyCheckEqual(…rest));
}

增强的对象语法

1. 属性值简写

当属性名与变量名一样时:

let name = 'Matt';
let person = {
	name
};
console.log(person); // { name: 'Matt' }

2.可计算属性

有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为JavaScript 表达式而不是字符串来求值:

const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;

function getUniqueKey(key) {
	return `${key}_${uniqueToken++}`;
}

let person = {
	[getUniqueKey(nameKey)]: 'Matt',
	[getUniqueKey(ageKey)]: 27,
	[getUniqueKey(jobKey)]: 'Software engineer'
};

console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }

3.简写方法名

在给对象定义方法时,通常都要写一个方法名、冒号,然后再引用一个匿名函数表达式,如下所示:

let person = {
	sayName: function(name) {
		console.log(`My name is ${name}`);
	}
};
person.sayName('Matt'); // My name is Matt

let person = {
	sayName(name) {
		console.log(`My name is ${name}`);
	}
};
person.sayName('Matt'); // My name is Matt

4.对象解构

可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。通过匹配来获取值

// 使用对象解构
let person = {
	name: 'Matt',
	age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
  • 解构赋值不一定与对象的属性匹配,当引用的属性不存在时,会赋值undefined:
  • 可以在解构赋值的同时定义默认值,若匹配成功会覆盖默认值:
  • 当解构值的数量与对象的属性数量不等时,若值的数量小于属性数量会赋值前面的属性,若大于则会报错。
  • 解构并不要求变量必须在解构表达式中声明。

解构在内部使用函数ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。
即会把上例中的person视为一个对象进行处理
这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据ToObject()的定义),null和undefined 不能被解构,否则会抛出错误(这两个类型不能被当成对象进行处理)。
 

let { _ } = null; // TypeError
let { _ } = undefined; // TypeError

5.嵌套解构

解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:

let person = {
	name: 'Matt',
	age: 27,
	job: {
		title: 'Software engineer'
	}
};
let personCopy = {};

({
	name: personCopy.name,
	age: personCopy.age,
	job: personCopy.job
} = person);

// 因为一个对象的引用被赋值给personCopy,所以修改
// person.job 对象的属性也会影响personCopy
person.job.title = 'Hacker'

console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }

console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }

需要注意的是,涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分

在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响arguments对象

创建对象 

虽然使用 Object 构造函数或者对象字面量来创建单一对象非常方便,但是不方便创建多个对象。在ES6 之前,没有正式地支持面向对象构造函数----比如类或者继承。不过接下来,你会看到这一过程如何逐渐成功演变。

  • 工厂模式 【factory pattern】
  • 构造函数模式 【constructor pattern】
  • 原型模式 【prototype pattern】

构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在ECMAScript 中区分构造函数和普通函数。

工厂模式

工厂模式在软件设计中非常流行,只要有返回新对象的函数,它不是构造函数或者类,那么就是工厂模式。

unction 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("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");

工厂模式解决了批量创建对象的问题,但是没有解决对象识别问题,所有对象原型都是 Object, 无法识别 Array Data 等类型。

 构造函数模式

ECMAScript 里面有原生的构造函数比如 ObjectArray ,同时也可以自定义构造函数,比如上面工厂函数的例子可以写成

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");

console.log(person1 instanceof Person);    // true,解决了工厂模式中不能识别的问

构造出来的对象的constructor属性会指向原函数:通过这种方式创造出的既是Object的实例又是Person的实例,因此可以体现出对象之间的差异,将对象标识为特定的一种类型。

 构造函数也是函数

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

若直接作为函数调用,会将这些属性添加到window对象

new操作符

使用 new 操作符来新建对象实例,调用构造函数会执行如下操作:

在内存中创建一个新对象。
这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype 属性。
构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
执行构造函数内部的代码(给新对象添加属性)。
如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

构造函数的问题 ❤❤❤❤❤❤❤

构造函数的缺点是新建每个实例方法都被创建了一次,比如上面的 person1 和 person2 都有一个方法名叫 sayName(),但是这两个方法不是同一个 Function 构造函数的实例。

逻辑上来说,上面的构造函数长这样

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = new Function("console.log(this.name)"); // logical equivalent
}

 也就是说,每一个Person 实例的sayName 方法都有一个自己的 Function 构造函数,这种重复是没有必要的,

原型模式

每一个新建的函数都有 prototype 属性,这个 prototype 对象包含了通过同一构造函数创建的所有实例共享的属性和方法。同样的原型模式可以使用函数声明和函数表达式两种形式。

function Person() {}
      
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
 console.log(this.name);
};
      
let person1 = new Person();
person1.sayName();  // "Nicholas"
      
let person2 = new Person();
person2.sayName();  // "Nicholas"
      
console.log(person1.sayName == person2.sayName); // true

在这里,sayName() 方法是挂载在原型对象上,构造函数为空。和2.2构造函数模型不一样的是,原型模式的实例共享所有的属性和方法

原型是如何工作的

 无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype 属性(指向原型对象)。原型对象只会默认获得一个名为constructor 的属性,指回与之关联的构造函数(相等于指向构造函数Person),并会默认继承来自于Object的方法(会继承一个属性和多个方法)

正常的原型链都会终止于Object 的原型对象 Object 原型的原型是null 

使用Object.getPrototypeOf()可以方便地取得一个对象的原型,返回参数的内部特性[[Prototype]]的值

setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值(影响性能)

可以通过Object.create()来创建一个新对象,同时为其指定原型:

 原型层级 

每次在访问属性时,总是先在实例上搜索该属性是否存在,不存在则继续在原型上搜索。如果实例和原型同时都有该属性,实例的优先级更高,也就是说在实例上找到了该属性就不再往原型上去找了,即便是该属性值为null。

但是如果使用 delete 操作符移除了该属性,那么搜索会继续走向原型。

function Person() {}
      
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
 console.log(this.name);
};
      
let person1 = new Person();
let person2 = new Person();
      
person1.name = "Greg";
console.log(person1.name); // "Greg" - 来自实例 instance
console.log(person2.name); // "Nicholas" - 来自原型 prototype

hasOwnProperty 方法可以判断属性是存在实例上(返回true)还是原型上(返回false)。

在被遮蔽时,实例上调用hasPrototypeProperty()返回false。不过,使用delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象

ECMAScript 的Object.hasOwnPropertyDescriptor()方法只对实例属性有效。

要取得原型属性的描述符,就必须直接在原型对象上调用Object.getOwnPropertyDescriptor()。如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined。

原型和in操作符 

主要有两种方式使用 in 操作符:直接在对象上操作和在 for-in 循环中操作。

直接在对象上使用,会同时检测实例上和原型上是否存在该属性。

function Person(){}

Person.prototype.name = "God";

let abby = new Person();

abby.hasOwnProperty("name");   // false 只检测实例上
console.log("name" in abby);   // true  会同时检测实例上和原型上是否存在该属性

使用这个特性还可以检测只存在原型上的属性

function hasPrototypeProperty(object, name){
  return !object.hasOwnProperty(name) && (name in object);
}  

使用 for-in 循环时,会返回所有可以枚举的对象属性,包括实例上的和原型上的。

Object.keys

Object.keys()方法会返回一个由一给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致

function Person() {}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
 console.log(this.name);
};

let keys = Object.keys(Person.prototype);
console.log(keys); // ["name", "age","job", "sayName"]
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // ["name", "age"]

Object.getOwnPropertyNames

如果想得到包括不可枚举的属性,使用 Object.getOwnPropertyNames() 方法,注意 constructor 是不可枚举属性

let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // ["constructor", "name","age","job", "sayName"]

Object.keys()Object.getOwnPropertyNames() 都是 for-in 循环很好的替代品

 Object.getOwnPropertySymbols()

针对 ES6 新增的 Symbols 类型,Object.getOwnPropertyNames() 方法不再奏效,因为Symbols 没有属性名一说,所以引入了 Object.getOwnPropertySymbols() 

let k1 = Symbol('k1'),
    k2 = Symbol('k2');

let o = {
 [k1]: 'k1',
 [k2]: 'k2'
};

console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]

属性枚举顺序

for-in循环,Object.keys(),Object.getOwnPropertyNames/Symbols(), 和Object.assign() 在枚举顺序上有很大的不同。

前两个在枚举顺序上是随机的,这是由JavaScript 引擎和浏览器决定的。

后两者则有确定的枚举顺序:数字类型按照增序,字符串和symbols 类型按照插入顺序,在对象字面量里面的会按照逗号分隔顺序

对象迭代

在JavaScript 很长的历史中,迭代对象属性都不太方便。 ECMAScript 2017 引入了两个静态方法 Object.values()Object.entries() 可以将对象内容转化为序列化的可迭代的形式。

onst o = {
 foo: 'bar',
 baz: 1,
 qux: {}
};

console.log(Object.values(o));
// ["bar", 1, {}]

console.log(Object.entries((o)));
// [["foo", "bar"], ["baz", 1], ["qux", {}]]

 另一种原型语法

注意到上面每次定义原型属性和方法都把 prototype 写了一遍,也可以简写为

function Person() {}
Person.prototype = {
	name: "Nicholas",
	age: 29,
	job: "Software Engineer",
	sayName() {
		console.log(this.name);
	}
};

但这种写法相当于为Person.prototype这个属性赋了新的值,即重写了默认的原型对象。之前的写法是直接修改属性,所以不会影响。

这种写法会将一个新对象赋给Person.prototype,而不再是随着构造函数一起生成的对象,所以这个对象的constructor属性不再是Person函数,而是Object函数

如上所示,使用构造函数生成一个对象实例friend,它的属性继承于原型对象,也包括constructor属性,因此值是Object函数。

但可以通过使用其他方法来定义constructor 属性为构造函数

function Person() {
}
      
Person.prototype = {
 constructor: Person,  // ⚠️ 
 name: "Nicholas",
 age: 29,
 job: "Software Engineer",
 sayName() {
  console.log(this.name);
 }
}; 

// 这种方式等同于
// restore the constructor
Object.defineProperty(Person.prototype, "constructor", {
 enumerable: false,
 value: Person
});

 原型的动态性

因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对
象所做的修改也会在实例上反映出来。下面是一个例子:

let friend = new Person();
Person.prototype.sayHi = function() {
	console.log("hi");
};
friend.sayHi(); // "hi",没问题!

即使在实例定义之后修改原型对象也会在实例中反映出来

之所以会这样,主要原因是实例与原型之间松散的联系。在调用friend.sayHi()时,首先会从这个实例中搜索名为sayHi 的属性。在没有找到的情况下,运行时会继续搜索原型对象。因为实例和原型之间的链接就是简单的指针,而不是保存的副本,所以会在原型上找到sayHi 属性并返回这个属性保存的函数。

但是重写原型对象并不会反映在实例中,因为实例中指向原型对象的指针[[Prototype]]是在实例生成时自动赋值的,因为重写原型对象并不会影响指针,所以实例中的方法和属性还是旧的原型对象上的

function Person() {}
Person.prototype = {
	name: "Nicholas",
	age: 29,
	job: "Software Engineer",
	sayName() {
		console.log(this.name);
	}
};

let friend = new Person();

Person.prototype = {
	name: 'newName',
	sayName() {
    console.log(this.name);
	}
};

friend.sayName();  // Nicholas

原生对象原型 

所有原生引用类型的构造函数(包括Object、Array、String 等)都在原型上定义了实例方法。

通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型,因此随时可以添加方法。
比如,下面的代码就给String原始值包装类型的实例添加了一个startsWith()方法:

String.prototype.startsWith = function (text) {
	return this.indexOf(text) === 0;
};
let msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true

 原型的问题

原型模式弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。

原型的最主要问题源自它的共享特性,当属性包含引用值时,在一个实例上进行修改会导致其他实例的该属性同样会被修改。

浅复制的问题

function Person() {}
Person.prototype = {
	constructor: Person,
	name: "Nicholas",
	age: 29,
	job: "Software Engineer",
	// 引用赋值
	friends: ["Shelby", "Court"],
	sayName() {
		console.log(this.name);
	}
};

let person1 = new Person();
let person2 = new Person();

person1.friends.push("Van");
// 同时被修改
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
// 指向的是同一个值
console.log(person1.friends === person2.friends); // true

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值