一、概念
一般来说软件开发模式有两种,一种是面向对象,一种是面向过程。面向过程它只是完成自己所需要的操作,这种思想简洁方便,但是这种设计缺少可维护性。而面向对象它本质上是组件化设计(模块化思想),方便局部维护但设计上要求规范比较多,也就是说模块化设计最重要的就是标准,以及整个项目的整体把控。
面向对象有以下几个特点:
- 封装性:保护内部的操作对外不可见;
- 继承性:相当于一代代的传承问题;
- 多态性:在一个范围内的定义改变;
JavaScript可以模拟实现继承和封装,但不能模拟实现多态,所以js是基于事件,基于对象的语言。
面向对象编程(Object Oriented Programming)
简称”OOP”,是一种编程开发思想,它将真实世界的各种复杂关系抽象成一个个对象,然后由对象之间的分工合作完成对现实世界模拟。
类和对象的概念:
- 类:具有相同特征(属性)和行为(方法)的集合。
- 如:人类->:属性:姓名、身高、星座 ; 方法-> 吃、喝、玩
- 对象:从类中拿出具有确定属性值和方法的个体叫做对象。
- 可以这样说,对象是包含属性和方法的集合,万物皆对象。
- 二者关系:类是抽象的,对象是具体的,类是对象的抽象化,对象是类的具体化。
二、对象
ECMA-262将对象定义为一组属性的无序集合,严格来说,这就意味着对象是一组没有特定顺序的值。我们可以将对象想象成一张散列表,其中的内容就是一组组键值对,值可以是数据或者是函数。
1.1 属性类型
ECMA-262使用一些内部特性来描述属性的特征。属性分为数据属性和访问器属性两种:
- 数据属性:包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置。有以下4个特性
- [[Configurable]]: 表示属性是否可以通过delete删除并重新定义。默认情况下为true;
- [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下为true;
- [[Writable]]:表示属性的值是否可以修改。默认情况下为true;
- [[Value]]:包含实际属性的值。默认值为undefined。
- 可以通过Object.defineProperty() 方法来修改属性的默认特性
let person = new Object();
person.name = 'amy';
person.age = 20;
person.say = function () {
console.log('hello')
}
/*
* Object.defineProperty()
* params: 要给其添加的属性对象 属性的名称 描述符对象(可多个)
* 在调用Object.defineProperty()时,configurable,enumerable和writable值如果不指定则默认为false
*/
Object.defineProperty(person, 'age', {
writable: false,
value: 22
});
console.log(person.age); //22
复制代码
- 访问器属性:不包含数据值,它包含一个获取函数(getter)和设置函数(setter)。它也有4个特性描述其行为:
- [[Configurable]]: 表示属性是否可以通过delete删除并重新定义。默认情况下为true;
- [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下为true;
- [[Get]]:获取函数,在读取属性时调用,默认值为undefined;
- [[Set]]:设置函数,在写入属性时调用,默认值为undefined;
- 访问器属性同样是不能直接定义的,要通过Object.defineProperty()方法。
- 可以通过Object.defineProperties() 方法通过描述符一次性定义多个属性
let obj = {};
Object.defineProperties(obj, {
'defaultYear': {
value: 2021,
writable: true,
},
'edition': {
value: 1,
writable: true,
},
'year': {
get() {
return this.defaultYear;
},
set(newYear) {
if (newYear > 2021) {
this.edition = newYear - 2021;
}
}
}
})
obj.year = 2025;
console.log(obj.edition) //4
/*
* Object.getOwnPropertyDescriptor()方法
* @params: 2个参数:属性所在的对象和要取得其描述的属性名
*/
console.log(Object.getOwnPropertyDescriptor(obj, 'defaultYear'));//{value: 2021, writable: true, enumerable: false, configurable: false}
复制代码
还可以通过Object.getOwnPropertyDescriptor() 方法获取指定属性的属性描述符。
es6一共有5种方法可以遍历对象的属性:
- for ……in :它可以循环遍历对象自身和继承的可枚举属性(不包含symbol)
- Object.keys(obj) :返回一个数组,包括对象自身的不包含继承的所有可枚举属性(不含symbol)
- Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性,包括不可枚举(不含symbol)
- Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身所有Symbol属性
- Reflect.ownKeys(obj):返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举。
1.2 对象常用方法
对象还有一些其它常用方法如下所示:
- Object.assign() :将所有可枚举属性从一个或多个源对象分配到目标对象,并返回目标对象。可用于对象的浅拷贝。
/*
* Object.assign(target,...sources);
* @params:
* target:目标对象
* sources:源对象
* return: 目标对象
*/
// 拷贝对象(拷贝的是可枚举属性值)
const obj = { a: 1, b: [3], c: { name: 'amy' } };
const copyObj = Object.assign({}, obj);
console.log(copyObj); //{ a: 1, b: [3], c: { name: 'amy' } }
//合并对象(可合并具有相同属性的对象)
const a1 = { a: 1, b: 4 }, a2 = { b: 2 }, a3 = { c: 3, b: 5 };
const a = Object.assign(a1, a2, a3);
console.log(a);//{a: 1, b: 5, c: 3}
复制代码
- Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
/*
* Object.create(proto,[propertiesObject]);
* @params:
* proto:新创建对象的原型对象
* [propertiesObject]:可选 用以对对象的属性进行进一步的描述。
* return: 一个新对象,带着指定的原型对象和属性。
*
* Object.create()方法创建对象时,属性是在原型下面的,可以直接访问
*/
let obj = {
name: 'amy',
print: function () {
console.log(`my name is ${this.name}`);
}
}
let me = Object.create(obj);
console.log(me) //{}
console.log(me.__proto__); //{name: "amy", print: ƒ}
console.log(me.name); //amy
me.print(); ////my name is amy
复制代码
- Object.is():判断两个值是否为同一个值,它与==或者是===两个运算符都不一样,Object.is()不会强制转换两边的值,并且===运算符将数字-0和+0视为相等,而Object.is()则不是,它更加严格。
/*
* Object.is(value1,value2)
* @params:value1:被比较的第一个值;value2:被比较的第二个值
* return:一个Boolean类型标示两个参数是否是同一个值
*/
console.log(Object.is(undefined, undefined));//true
console.log(Object.is(+0, -0)); //false
console.log(Object.is(NaN, NaN)); //true
复制代码
- Object.keys():返回一个由一个给定对象的自身可枚举属性组成的数组,这些属性的顺序与手动遍历该对象属性时一致。;
/*
* Object.keys(obj)
* @params:obj要返回其枚举自身属性的对象
* return: 一个表示给定对象的所有可枚举属性的字符串数组
*/
let obj = {};
Object.defineProperties(obj, {
'age': {
value: 20,
writable: true,
enumerable:true,
},
'name': {
name: 'ph',
enumerable: false
}
})
console.log(Object.keys(obj)); //["age"]
复制代码
- Object.values():返回一个给定对象自身的所有可枚举属性值的数组;
/*
* Object.values(obj)
* @params:被返回可枚举属性值的对象
* return: 一个包含对象自身的所有可枚举属性值的数组
*/
let obj = {};
Object.defineProperties(obj, {
'age': {
value: 20,
writable: true,
enumerable: true,
},
'name': {
name: 'ph',
enumerable: false
}
})
console.log(Object.values(obj)); //[20]
复制代码
- Object.entries():返回一个给定对象自身可枚举属性的键值对数组;
/*
* Object.entries(obj)
* @params:可以返回其可枚举属性的键值对的对象
* return:给定对象自身可枚举属性的键值对数组
*/
let obj = {};
Object.defineProperties(obj, {
'age': {
value: 20,
writable: true,
enumerable: true,
},
'name': {
name: 'ph',
enumerable: false
}
})
console.log(Object.entries(obj)); //["age", 20]
// new Map() 构造函数接受一个可迭代的entries
let map = new Map(Object.entries(obj));
console.log(map); //Map(1) {"age" => 20}
复制代码
- Object.prototype.toString():返回一个表示该对象的字符串
/*
* Object.prototype.toString(obj)
* return:一个表示该对象的字符串
* obj.toString() vs Object.prototype.toString.call()
* toString:返回这个对象的字符串,它是Object的原型方法
* 而Array,Function是Object的实例,它们重写了toString方法,此时调用toString方法调用的是重写后的toString方法,而不是Object上原型toString方法,
* 所以使用obj.toString()只能将obj转换为字符串,要想检测真正的数据类型还是要用到Object原型上的方法。
*/
// 使用Object.prototype.toString()方法会返回一个形如[object type]的字符串,其中type就代表着其类型
console.log(Object.prototype.toString.call(undefined)); //[object Undefined]
console.log(Object.prototype.toString.call(null)); //[object Null]
// toString方法
// console.log(null.toString()); //Cannot read property 'toString' of null
// console.log(undefined.toString()); //Uncaught TypeError: Cannot read property 'toString' of undefined
/*
* 数据类型检测的方法:
* 1、typeof:返回结果是一个字符串,字符串中包含了对应的数据类型
* 它能检测如下类型,即:number,string,boolean,undefined,bigInt,function
* 其它类型检测的结果都为:'object'
*
* 2、instanceof:检测当前实例是否属于这个类,一般只能用于普通对象,数组对象等,但是它不能证明instanceof object是true就是普通对象,不能用于基本类型的检测
*
* 3、constructor:获取实例的构造函数,基于这一点可以用于数据类型检测。但是constructor它是可以随意修改的,不准
* 4、Object.prototype.toString.call(obj):最好用
*/
/* 1、typeof */
console.log(typeof typeof typeof ([1])) //string
console.log(typeof function () { });//function
console.log(typeof null); //object
/* 2、instanceof */
let arr = [];
const n = 10;
const m = new Number(10);
console.log(arr instanceof Array);//true
console.log(arr instanceof Object); //true 不能证明instanceof Object是true的就是普通对象
//n 是Number的一个实例,只不过它是字面量方式创建出来的原始类型
console.log(n instanceof Number); //false
//m 是通过new出来的,也是Number的一个实例,它是构造函数创建出来的引用类型
console.log(m instanceof Number); //true
// 当原型重定向时:虽然p基于__proto__可以找到Array.prototype但p不具备数组的任何特征,所以它不是一个数组,但用instanceof 检测时它又为true这就是相互矛盾的点
function Person() { };
Person.prototype = Array.prototype;
let p = new Person;
console.log(p instanceof Array); //true
/* 3、constructor */
let ary = [];
console.log(ary.constructor === Array); //true
function Fn() { }
Fn.prototype = Array.prototype;
let f = new Fn;
console.log(f.constructor === Array);//true 一旦重定向后,constructor修改了,那也就不准了
复制代码
三、创建对象
3.1 字面量方式
这是创建对象最简单的方式。对象字面量是由若干键值对组成的映射表,键值对中间用冒号分隔,键值对之间用逗号分开,整个的映射表用花括号括起来。属性名可以是javascript标识符也可以是字符串直接量。属性值可以是任意类型的javascript表达式。
如果不同的变量指向同一个对象,那它们都是这个对象的引用,也就是说指向同一个堆内存地址,修改其中一个变量会影响到其它所有变量,如果取消某个变量对于原对象的引用则不会影响到另外变量。
let person = {
name:'amy',
age:20,
fn:function(){
console.log('fn');
}
};
let personCopy = person;
personCopy.age = 30;
console.log(person.age); //30
person = null;
console.log(personCopy);
复制代码
可以通过点(.)或者是方括号([])运算符来获取属性值:
- 点(.)运算符:点运算符是很多面向对象语句通用写法,简单常用。通过点运算符访问对象属性时,属性名用一个标识符来表示,标识符要符合变量命名规则。
- 方括号([])运算符:通过方括号运算符来访问对象属性时,属性名用字符串来表示。
- 可以通过变量来访问属性,属性名称可以是特殊的标识符。
- 在方括号运算符内可以使用表达式,es6中新增了可计算属性名,可以在使用[]时用一个表达式来当成属性。
- 查询一个不存在属性时不会报错,而是返回undefined。但是如果是对象不存在,再查询这个不存在对象的属性就会报错。可以利用这一点,来检查一个全局变量是否被声明。
let a = 1;
let person = {
[a+2]:'abc',
'font-size':'12',
}
console.log(person['font-size']); //12
console.log(person[3]); //abc
复制代码
对象属性相应的值可以通过赋值语句来修改:
- 如果属性名存在于对象,那这个属性的值会被替换
- 如果对象之前没有这个属性名,那么这个属性会被扩展到对象中
我们可以通过delete删除对象属性,但是用delete删除数组元素时,它可以删除相应的自有属性,不能改变数组的长度(不能删除继承属性)。
var obj = {
'first-name': 'ph',
'second-name': 'davina',
array:[1,2,3,4]
};
obj['first-name'] = 'amy';
obj.lastName = 'lily';
console.log(obj);
console.log(delete obj.array[2]); //true
console.log(obj.array.length); //4
复制代码
3.2 使用Object创建对象
使用new关键字,创建一个Object的实例对象,然后再为其添加属性和方法。
var person = new Object();
person.name = 'lily';
person.age = 20;
person.sayHello = function () {
console.log('hello world');//hello world
}
person.sayHello();
复制代码
3.3 工厂模式
虽然使用构造函数或者是字面量都可以创建单个对象,但如果是使用同一接口创建很多对象,这就会产生大量的重复代码,所以我们用工厂模式,把实现同一件事的相同代码放到一个函数中,以后如果再想实现这个功能,不需要重新编写代码,只需要执行当前的操作就可,用函数来封装以特定接口创建对象。
function createPerson(name, age) {
let obj = {};
obj.name = name;
obj.age = age;
obj.personInf = function () {
console.log('我的名字是' + this.name + ',今年' + this.age + '岁');
}
return obj;
}
let p1 = createPerson('amy', 12).personInf();
console.log(p1);
复制代码
从中我们可以看出工厂模式的优点:在工厂模式中,我们只需要知道所要生产的具体东西,无须关心具体创建过程,如果增加新产品只需要添加一个具体产品类和对应的实现工厂,无需对原工厂进行任何修改,很好符合了”开闭原则”。但是每增加一个产品时都需要增加一个具体的类和对象,实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
3.4 构造函数
在javascript中,构造函数就是一个用来生成对象的函数,所有的对象都是由它创建,像object和Array这样原生的构造函数运行时可以直接在执行环境中使用。当然我们也可以自己定义构造函数,以函数形式为自己的对象类型定义属性和方法。构造函数首字母一般大写,它与普通函数的区别在于是否由new操作符调用 。new是语法糖,使用new操作符调用构造函数时,会创建一个新对象,将this指向该对象(将构造函数的作用域赋给新对象)。
在普通函数执行的基础上加了“new xxx()”,这样的话就不仅仅是普通函数执行而是构造函数执行,当前函数名称为“类名”,接收的返回结果是当前类的实例,实例由构造函数生成,平时用的都是实例化对象。简单来说,通过构造函数new出来的的对象叫做实例,创建对象的过程叫做实例化。
function CreatePerson(name, age) {
this.name = name;
this.age = age;
this.personInf = function () {
console.log('我的名字是' + this.name + ',今年' + this.age + '岁');
}
}
let person1 = new CreatePerson('amy', 20);
person1.personInf();
复制代码
构造函数 vs 普通函数:
-
- 构造函数和普通函数执行大体上是一致(它具备普通函数的一面);
-
- 区别在于使用new操作符调用时,首先会在内存中默认创建一个新对象(这个对象就是当前类的实例),这个新对象内部的[[prototype]]特性被赋值为构造函数的prototype属性,让上下文中的this指向这个新对象;
-
- 构造函数执行,不写return,浏览器会默认返回创建的实例,但是如果我们自己写了return分为两种情况:
- 3.1 return的是基本值,返回的结果依然是类的实例,没有受到影响;
- 3.2 return的是引用值,则会把默认返回的实例覆盖,此时接收到的结果就不在是当前类的实例;
- 构造函数执行时,尽量减少return的使用,防止覆盖实例;
-
- 用new调用的函数,这个函数就是一个用来创建对象的函数即构造函数,它得到的结果永远是一个对象,不管函数有无返回值;
function Func(x, y) {
// num只是当做普通函数执行时,给私有上下文中设置的私有变量,和实例对象没有关系,只有this是实例对象,所以this.XXX=XXX才和实例有关系
let num = x + y;
this.x = x;
this.y = y;
// return {
// name: 'xxx'
// }; //=>返回基本类型值,f2依然是创建的实例对象;如果自己返回的是一个引用值,一切以自己返回的为主,此时f2={name:'xxx'}而不再是当前类的实例
}
let f2 = new Func(10, 20);
console.log(f2);
复制代码
我们可以用instanceof检测当前对象是否为某个类的实例,而且构造函数不一定要写成函数声明形式,赋值给变量的函数表达式也是可表示构造函数。在实例化时如果不想传递参数,那么构造函数后面的括号可写可不写,只要有new操作符就可以调用相应函数,如下所示:
let Person = function (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
let p1 = new Person;
let p2 = new Person('amy', 20);
console.log(p1 instanceof Person); //true
console.log(p2 instanceof Person); //true
p2.sayName(); //amy
复制代码
从上面我们可以看到构造函数主要问题在于其定义的方法会在每个实例上都创建一遍,p1和p2身上都有sayName()方法,这两个方法不是同一个Function实例要做的事件相同,这样就重复了。所以我们可以将这个函数定义转移到构造函数外部,如下所示:在构造函数内部,sayName属性它是等于全局sayName()函数,但这样做全局作用域也被扰乱,而且sayName函数只能在一个对象上调用,如果这个对象需要多个方法,那就需要在全局作用域中定义多个函数了,这样就不太友好可以用原型模式解决。
let Person = function (name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
function sayName() {
console.log(this.name);
}
}
let p1 = new Person('lily',18);
let p2 = new Person('amy', 20);
p1.sayName(); //lily
p2.sayName(); //amy
复制代码
3.5 原型模式
在了解原型模式前我们先要了解什么是prototype(原型)?什么是constructor?
prototype(原型):每一个函数(箭头函数除外)都具备prototype属性,属性值是一个对象。指向了当前函数所在的引用地址。在这个对象中会存储当前类的公共属性和方法;
constructor:在prototype的堆内存中,如果是浏览器为其默认开辟的堆内存,会存在一个内置的属性constructor,属性值指回当前类本身;
proto:每一个对象都有一个内置属性:proto(原型链属性),属性值是当前实例所对应类的prototype。
原型链 :调用当前实例对象的某个属性(成员访问),先看自己私有属性中是否存在,存在就调用自己私有的;不存在,则默认按照__proto__找所属类prototype上的公有属性和方法;如果还没有,再基于prototype身上的__proto__继续向上级查找,直到找到Object.prototype为。因为Object.prototype它也是一个普通对象,它也是Object的实例,理论上来说它的__proto__指向Object.prototype也就是指向自己,这样做就没有了意义,所以Object.prototype.__proto__为null。
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
Fn.prototype.getX = function () {
console.log(this.x);
}
let f1 = new Fn();
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.getX());
console.log(f1.__proto__.getX());
复制代码
从上面我们可以看出,构造函数,原型对象和实例是3个完全不同的对象。实例可以通过__proto__链接到原型对象,实际上是指向隐藏的[[prototype]],我们可以看到构造函数通过prototype属性链接到原型对象,实例与构造函数没有直接联系,但是与原型对象有直接联系。
我们可以通过isProtoypeOf()方法检查某个对象是否存在于另一个对象的原型链上,或者是使用Object.getPrototypeOf()方法返回指定对象的原型,或者是Object.sePrototypeOf()方法设置一个指定的对象的原型。
function Fn() { };
let f1 = new Fn();
let f2 = new Fn;
console.log(Fn.prototype);
/*
*{constructor: ƒ}
* constructor: ƒ Fn()
* __proto__: Object
*/
// 构造函数的prototype属性引用其原型对象,原型对象里的constructor引用这个构造函数
console.log(Fn.prototype.constructor === Fn); //true
// 正常的原型链终于于Object的原型对象
console.log(Fn.prototype.__proto__ === Object.prototype);//true
console.log(Fn.prototype.__proto__.__proto__ === null); //true
// 实例通过__proto__链接到原型对象
console.log(f1.__proto__ === Fn.prototype);//true
console.log(f1.__proto__.constructor === Fn); //true
console.log(f1.__proto__.__proto__ === Object.prototype);//true
//同一个构造函数创建的两个排实例共享同一个原型对象
console.log(f1.__proto__ === f2.__proto__);//true
//instanceof:检查实例的原型链中是否包含指定的构造函数的原型
console.log(f1 instanceof Fn); //true
// isPrototypeOf()
console.log(Fn.prototype.isPrototypeOf(f1));
// getPrototypeOf()
console.log(Object.getPrototypeOf(f1) == Fn.prototype);
复制代码
原型指向是可以重定向的,在我们重定向原型指向时,之前原型上的内容会丢失(包含constructor属性)为了保证结构完整性,我们一般要手动设置constructor属性。
function fun(){
this.a=0;
this.b=function(){
alert(this.a);
}
}
fun.prototype={
b:function(){
this.a=20;
alert(this.a);
},
c:function(){
this.a=30;
alert(this.a)
}
fun.prototype.constructor = fun;
}
var my_fun=new fun();
my_fun.b();
my_fun.c();
复制代码
我们可以向内置类的原型上扩展方法,因为内置类的原型上会默认很多常用的方法,但是在真实的项目开发中这些内置方法往往不足以完成开发要求,所以我们向内置类的原型上扩展方法,这样调用起来比较的方便。在方法执行时,方法中的this就是当前处理的那个实例。
在向内置类的原型上扩展方法时我们要注意以下几点:
-
- 扩展的方法名字最好设置前缀,如MyXXX,防止自己扩展的方法替换了内置原来的方法;
-
- this的结果一定是对象数据类型值,所以向基本数据类型的原型上扩展方法,方法被执行时,方法中的this不在是基本类型,但是还是按照原始的方式处理即可
-
- 如果返回的结果依然是当前类的实例,还可以继续调用当前类原型上其它的方法(如果不是自己类的实例,可以调用其它类原型上的方法) => “链式写法”。
String.prototype.queryURLParams = function queryURLParams(key) {
// this -> 当前要处理解析的URL
let obj = {};
this.replace(/([^?&=#]+)=([^?&=#]+)/g, (_, $1, $2) => obj[$1] = $2);
this.replace(/#([^?&=#]+)/g, (_, $1) => obj['_HASH'] = $1);
return typeof key === "undefined" ? obj : obj[key];
};
let url = "https://support.google.com/chrome/?p=help&ctx=keyboard";
let result = url.queryURLParams();
console.log(result); //{p: "help", ctx: "keyboard"}
String.prototype.indexOf = function indexOf() {
return 'OK';
};
console.log(url.indexOf('?')); //OK
复制代码
对于一个对象来说,它的属性方法(私有的/公有的)存在“枚举”的特点:在for……in循环的时候是否可以遍历到,能遍历到的是可枚举的,不能遍历到的是不可枚举的。
// func是可枚举的属性了
Object.prototype.func = function func() {};
let obj = {
name: 'davina',
age: 18
};
for (let key in obj) {
// 在遍历的时候,对于原型上扩展的公共属性方法,我们过滤掉,只遍历对象中私有的属性方法(必须是可枚举的)
if (!obj.hasOwnProperty(key)) break;
console.log(key);
}
复制代码