(JavaScript - 原生)创建对象的方式 + 原型 + 原型链 + 对象的继承
说道JavaScript中的对象,无论是从业多年的老鸟还是刚刚接触的菜鸟都不会陌生。本篇文章专注于揭开JavaScript对象的神秘面纱,带领大家细品其中的韵味,真正做到深入浅出。
> -1).对象的简介这个就不在多说了,就是一组无序键值对儿的集合,针对于这个特性我们的开发人员可谓是如鱼得水 ...
> -2).创建对象的几种方式
①.字面量式:
let obj_1 = {
name: '太阳',
age: 21
};
②.JavaScript内置Object构造器:
let obj_2 = new Object();
③.自定义构造器:
这里先阐述一个误区,很多人认为只有这种形式的才是构造器
function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.sayHello = function() {
console.log("Hello world ...");
}
}
其实不是这样的,如果你仅仅这么写了别的啥也没干,那么这个函数就是一个普通的函数。只有当你new它的时候它才真正履行了构造器的职责。换句话说这与函数的写法无关,只要你通过new操作符来new函数,任何函数都可以是构造器。
来,看看我们的对象是怎么产生的,并且再调用下sayHello方法。
let person = new Person('太阳', 21, "照亮世间 ...");
person.sayHello();
④.动态原型式:
说到原型我们一起来探究一下(什么是原型、作用是什么、怎么使用)。
Ⅰ.什么是原型?
表述:原型就是一个对象,是创建对象(实例)的那个构造器的原型对象,简称为原型。
// 构造器
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log("Hello World!");
};
}
// 构造器Person的原型对象
console.log(Person.prototype);
Person.prototype.see = function() {
console.log("看书 ...");
};
// 创建对象
let person = new Person("LJ", 21);
诚如大家所见,那个Person.prototype就是Perosn这个构造器的原型对象 -> 即Person的原型,真面目就是↓↓↓
Ⅱ.作用是什么?
原型的作用有二,其一是实现共享、其二是实现继承(继承我们后面细说)
说到共享这个作用,我们不得不提构造函数,当我们每次通过构造函数创建对象的时候这些属性在每个对象身上都有体现,并且如果其中的某一个甚至某几个属性或者方法是常用且不常更改的呢?难道我们每次都要在每个对象创建的时候都开辟空间去存储这些常用且不常更改的属性或者方法吗?
let person = new Person("LJ", 21);
let person_2 = new Person("XXY", 22);
console.log(person.sayHello === person_2.sayHello);
输出结果为:↓↓↓
这说明person对象与person_2对象分别存储着sayHello方法。
所以原型的共享特性正好可以解决这个问题,还记得我们在Person构造器的原型上定义了一个see方法吗?
Person.prototype.see = function() {
console.log("看书 ...");
};
我们可以试一下,看看我们定义在Person构造器原型上的see方法是只存储了一份供大家(众多由Person构造器创建的对象)使用的see方法呢?还是每个由Person构造器创建的实例都存储着一个see方法呢?
console.log(person.see === person_2.see);
输出结果为:↓↓↓
这说明see方法与上面的sayHello方法不同,sayHello方法是定义在每一个对象身上(浪费内存),而see方法是定义在Person构造器的原型身上内存中的see方法也独此一家,来供大家(众多由Person构造器创建的对象)访问并使用的(节省内存)。
但是我们还差了一个点没说,就是假如我们的对象身上有名字为的A方法其构造器原型上也有名字为A的方法那么以哪个为准?答案是以对象身上的那个A方法为准:
来我们贴下代码:
// 构造器
function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.sayHello = function() {
console.log("Hello World!");
};
}
// 构造器Person的原型对象
console.log(Person.prototype);
// 在构造器Person的原型对象上定义see方法
Person.prototype.see = function() {
console.log("看书 ...");
};
// 在构造器Person的原型对象上定义hobby属性
Person.prototype.hobby = "睡觉";
// 创建对象
let person = new Person("LJ", 21, "敲代码");
let person_2 = new Person("XXY", 22, "美丽");
console.log(person.sayHello === person_2.sayHello);
console.log(person.see === person_2.see);
console.log(person.hobby, person_2.hobby);
我们可以看到在Person构造器内部我们指定了hobby属性,言外之意等到new它的对象的时候,其对象身上就会带有hobby属性。但同时我们还在Person构造器的原型上定义了hobby属性。来我们看下打印结果:↓↓↓
这就说明:如果对象身上与其构造器原型上有着名字相同的属性或者方法以对象身上的为准,而原型上的会被覆盖掉,但不会消失。
Ⅲ.怎么使用?
至于怎么使用在已经贴出的代码中都已公布就不在赘述 ...
行,说到这咱们的动态原型的创建对象的方式就可以问世了。
function Obtain(name, age) {
this.name = name;
this.age = age;
if(typeof this.sayHello !== 'function') {// 当检测到对象身上没有sayHello方法的时候就在其构造器的原型上定义该方法,因为如果该对象身上有该方法你再往其构造器原型上定义也会被覆盖掉。
Obtain.prototype.add = function(x, y) {
return x + y;
};
}
}
let obt_1 = new Obtain("XXY", 21);
let obt_2 = new Obtain("YXX", 22);
console.log(obt_1.add(1, 2));
console.log(obt_1.add(2, 3));
console.log(obt_1.add === obt_2.add);
这样一来既可以批量生产对象,又可以节省内存,即每个对象的特性我们在构造器中直接定义即可,每个对象的共性我们就将其定义在其构造器的原型上。
> -3).原型链:
原型链的3种探究方式:
①.文字说明
构造器的prototype属性与构造器创建的对象的__proto__属性指向同一个原型对象。
在JavaScript中一切皆对象所以function(函数)也是对象,所以构造器也有__proto__属性只不过其与Function.prototype同指向Function的原型对象。
同时构造器的prototype属性与构造器创建的对象的__proto__属性指向的同一个原型对象作为对象也有__proto__属性与Object构造器的prototype属性和Object构造器所创建的对象的__proto__属性指向同一个原型对象,而这个原型对象的__proto__属性指向null。
Object构造器作为对象也有__proto__属性并与Function的prototype属性指向同一个原型对象,此原型对象的__proto__属性与Object的prototype属性指向同一个原型对象。
并且Function构造器的实例(对象)的__proto__属性与Object的prototype属性指向同一个原型对象。
每一个原型对象的contructor属性都指向他们的构造器。
②.代码说明
function Prototype(name, age) {
this.name = name;
this.age = age;
}
let obj = new Prototype('LJ', 21);
Prototype.prototype.hobby = "看书 ...";
console.log(Prototype.prototype);// 打印
console.log(obj.__proto__ === Prototype.prototype);// 打印 true
console.log(Prototype.__proto__ === Function.prototype);// 打印 true
console.log(Prototype.prototype.__proto__);// 指向Object的原型对象
console.log(Prototype.prototype.__proto__.__proto__);// 打印 null
let obj_2 = new Object();
console.log(obj_2.__proto__ === Object.prototype);// 打印 true
console.log(Prototype.prototype.__proto__ === Object.prototype);// 打印true
console.log(Object.__proto__ === Function.prototype);// 打印true
console.log(Function.prototype.__proto__ === Object.prototype);// 打印 true
let obj_3 = new Function();
console.log(obj_3.__proto__ === Function.prototype);// 打印true
// 并且我们凡是加在原型对象上面的属性或者方法,都会存储在Constructor.prototype所指向的构造器中、还有__proto__属性。
// Constructor.prototype === 实例.__prototype;
// Constructor.__proto__ === Function.prototype;
③.图片说明(最为直观)
简而言之 - 就是 每个对象都有__proto__属性、每个构造器都有prototype属性。他们俩指向同一个原型对象 ...,但是这个原型对象也具有__proto__属性继续 ...而且其构造器作为对象也具有__proto__ ....所有的原型对象的.constructor属性都指向其构造器。
在原型上添加的属性/方法在实例(对象)的构造器中都有体现。
> -4).对象的继承:
说到对象的继承给我们的大部分印象就是这样:
let obj = {
name: '太阳',
age: 21
};
let obj_copy = obj;// 浅拷贝
其实这样只能说是对象的浅拷贝,因为obj_copy并不是obj的副本,而是两者的引用相同,一个改变另一个也会发生变化。
所以我们应该通过一定的手段来避免这种事情发生,再者JavaScript的引用类型有好多(function、Object、Array、RegExp、Date ...)我们必须对这些类型负责,于是自己通过上面的一些知识写了一个关于对象继承的一个模式/方法。(实现了对象的继承即一个改变不会影响到另一个)。
上代码:↓↓↓
// 对象的继承
// 赋予入参对象调用原型上的extends方法权限
function enhance(subObj) {
// 入参子对象 - 引用类型
if(judgeType(subObj)) {
// 在子对象的原型上绑定extends方法
subObj.__proto__.extends = function (superObj) {
// 入参父对象 - 引用类型
if(judgeType(superObj)) {
// 当入参对象为Object类型
if(getType(subObj) === 'Object' && getType(superObj) === 'Object') {
// 对父对象进行深拷贝
for (let key in superObj) {
this.__proto__[key] = superObj[key];
}
// 返回拷贝后的子对象
return this;
// 当入参对象为Array类型
}else if(getType(subObj) === 'Array' && getType(superObj) === 'Array') {
// 拼接数组并返回
return [...this,...superObj];
// 当入参对象为RegExp类型
}else if(getType(subObj) === 'RegExp' && getType(superObj) === 'RegExp') {
// 对RegExp父对象进行深拷贝并返回
return new RegExp(superObj.source, superObj.flags);
// 当入参对象为Date类型
}else if(getType(subObj) === 'Date' && getType(superObj) === 'Date') {
// 创建新Date对象
let date = new Date();
// 获取其日期并设置
date.setTime(superObj.getTime());
// 返回新创建的Date对象
return date;
}
// 入参父对象 - 基本类型
}else {
// 抛出异常
throw new Error("CaseBy : Incoming type non reference types!");
}
};
// 入参子对象 - 基本类型
}else {
// 抛出异常
throw new Error("CaseBy : Incoming type non reference types!");
}
// 返回入参对象subObj,但此次extends方法已经在subObj对象的原型上了
return subObj;
}
// 获取指定对象的所属类型
function getType(arg) {
let str = Object.prototype.toString.call(arg);
let index = str.indexOf(" ");
if (index !== -1) {
str = str.substring(index + 1, str.lastIndexOf("]"));
}
// 返回指定类型
return str;
}
// 判断传入的元素是基本数据类型/引用数据类型
function judgeType(obj) {
if((typeof obj === 'object' && obj !== null) || typeof obj === 'function') {// 引用类型
return true;
}else {// 基本类型
return false;
}
}
let obj = new SubConstructor('LJ');// 创建子对象
function SubConstructor(name) {// 子对象构造器
this.name = name;
}
function SuperConstructor(watch) {// 父对象构造器
this.watch = watch;
}
// 创建父对象
let superObject = new SuperConstructor('铠甲勇士');
// 为父对象的原型上面添加一个foo方法
superObject.__proto__.foo = function () {
console.log(10);
};
// 测试Object
let obj_4 = enhance(obj);
let obj_5 = obj_4.extends(superObject);
console.log(obj_5.__proto__.watch = 133);
console.log(obj_5);
obj_5.foo();
console.log(superObject);
// 测试Array
let arr_2 = [1, 2];
console.log(enhance(arr_2).extends([1, 2, 3]));
// 测试RegExp
let reg_1 = /^/;
let reg_0 = /^[0-9a-zA-Z]+$/;
reg_1 = enhance(reg_1).extends(reg_0);
console.log(`继承完:${reg_1}`);
reg_1.compile(/^d+$/g);
console.log(`更改后:${reg_1}`);
console.log(`父正则${reg_0}`);
// 测试Date
let dateObj = new Date();
let dateObj_2 = new Date();
dateObj_2.setTime(1533451533856);
console.log(enhance(dateObj).extends(dateObj_2).getFullYear());
代码就不做过多讲解,只要顺着代码思维走并看注释理解是没有问题的 ...
上述代码实现的功能就是
对于正常的object对象会将其父对象(extends(父对象))中的所有属性遗传到子对象指向的原型上,子对象可以随时调用或者覆盖他们。
对于Array(数组),会将两个数组合并后返回。
对于RegExp(正则),会将子正则替换为父正则。
对于Date(时间),会深拷贝一份父Date对象中的时间,返回一个新的Date对象。
以上就是本篇文章的主要内容(对象的创建方式 -> 推荐最后一种、原型链、对象的继承)。
如果一步一步认真的看到最后相信你或多或少都会有些收获,如果看不懂就先保留着,等到时机成熟返回来再看就会明白了。
原创作品如果诸位觉得哪些地方不懂或者有问题欢迎在下面留言,我会在第一时间作答:
四个字 -> 脚踏实地 : 共勉!!!