写在前面

注:这个系列是本人对js知识的一些梳理,其中不少内容来自书籍:Javascript高级程序设计第三版和JavaScript权威指南第六版,感谢它们的作者和译者。有发现什么问题的,欢迎留言指出。

起因

Object构造函数、对象字面量、Object.creat都可以用来创建单个对象,但有明显缺点:使用同一个接口创建很多对象,会产生大量的重复代码。所以才开始了创建对象的模式的探索。

检测对象的类

3种常见的检测任意对象的类的技术:instanceof运算符、constructor属性、构造函数的名字。3种各有优劣,适用于不同场景。(但往往我们更关注对象可以完成什么工作,对象属于哪个类并不是最重要的)

  • instanceof运算符

    运算符左边是对象,右边是构造函数,如o instanceof f,如果在o的原型链中查找到f,就返回true:

var date = new Date();
console.log(date instanceof Date);//true
console.log(date instanceof Object);//true
console.log(date instanceof Number);//false

这种方式的缺点:①无法通过对象来获得类名,只能检测对象是否属于指定的类名,②如果是不同的执行上下文,如客户端中每个窗口和框架子页面都具有单独的执行上下文,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例。

  • constructor属性
console.log(date.constructor == Date);//true

缺点:①和instance的第2点缺点一样,执行上下文的问题,②并不是所有的对象都有constrctor属性,如果创建对象时把对象的prototype直接覆盖了而又没有指定constrctor属性,就会没有这个属性。

  • 构造函数的名称
//返回对象的类
function classof(o) {
return Object.prototype.toString.call(o).slice(8,-1);
}
//返回函数的名字(可能是空字符串),不是函数就返回null
Function.prototype.getName = function () {
if("name" in this) return this.name;
return this.name = this.toString().match(/function\s*([^()]*)\(/)[1];
}
function type(o) {
//type,class,name
var t,c,n; //处理null值特殊情况
if(o === null) return 'null';
//处理NaN和它自身不相等
if(o !== o) return "nan";
//识别出原始值的类型
if((t = typeof o) !== "object") return t;
//识别出大多数的内置对象(类名除了"Object")
if((c=classof(o)) !== "Object") return c;
//如果对象构造函数的名字存在,就返回它
if(o.constructor && typeof o.constructor === "function" && (n = o.constructor.getName()) ) return n;
//其他的类型无法判别,返回"Object"
return "Object";
}

这种方法的问题:①并不是所有的构造函数都有constructor属性,②并不是所有的函数都有名字(name是非标准属性),如果使用不带名字的函数定义表达式定义一个构造函数,getName()方法会返回空字符串。

//这种情况下如果没有name属性,那么getName()方法就返回空字符串了
var Example = function (x, y) {
this.x = x;this.y = y;
}

1.工厂模式

function creatPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
};
return o;
}
var person1 = creatPerson('jaychou',34,'singer');
var person2 = creatPerson('xiaoming',15,'student');
//{name: "jaychou", age: 34, job: "singer", sayName: ƒ}
console.log(person1);
//{name: "xiaoming", age: 15, job: "student", sayName: ƒ}
console.log(person2);

工厂模式的最大缺点:没有解决对象识别的问题,不知道一个对象的类型。

2.构造函数模式

显然构造函数可用来创建特定类型的对象,如Array,Date等,重写上面的例子:

function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
}
}
//{name: "jaychou", age: 34, job: "singer", sayName: ƒ}
var person1 = new Person('jaychou',34,'singer');
//{name: "xiaoming", age: 15, job: "student", sayName: ƒ}
var person2 = new Person('xiaoming',15,'student');

与之前对比:①没有显式地创建对象,②直接将属性和方法赋给了 this 对象,③没有 return 语句

使用new操作符调用Person构造函数后:

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(this指向了这个新对象)
  • 执行构造函数中的代码(为新对象添加属性和方法)
  • 返回新对象

类型的标识有了,构造函数模式的主要缺点是:每个方法都要在每个实例上重新创建一遍(函数也是对象的一种,导致重复创建对象了)

3.原型模式

  • 基本
function Person() {

}
Person.prototype.name = "jaychou";
Person.prototype.age = 34;
Person.prototype.job = 'singer';
Person.prototype.sayName = function () {
console.log(this.name);
}
var person1 = new Person();
person1.sayName();//jaychou
var person2 = new Person();
person2.sayName();//jaychou
console.log(person1.sayName == person2.sayName);//true

创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

//{name: "jaychou", age: 34, job: "singer", sayName: ƒ, constructor: ƒ}
console.log(Person.prototype);

我们打印了Person.prototype,默认情况下会有一个constructor属性,这个属性指向函数(在这里就是指向构造函数),其他的name,age,job,sayName属性和方法都是通过Person.prototype.添加进去的,所以由构造函数Person创建的实例都会包含这些属性和方法,它们是共享的。**

而且实例的内部包含一个指针(内部属性),指向构造函数的原型对象:[[Prototype]],在Firefox、Safari和Chrome中支持属性__proto__:

//{name: "jaychou", age: 34, job: "singer", sayName: ƒ, constructor: ƒ}
console.log(person1.__proto__);
console.log(Person.prototype == person1.__proto__);//true //打印对象的constructor:在这里person1的constructor就是Person
console.log(person1.constructor == Person);//true

另外,可以通过 isPrototypeOf 方法来确定对象之间是否存在原型关系:

console.log(Person.prototype.isPrototypeOf(person1));//true

还有,可以通过 Object.getPrototype 返回对象的原型:

//打印对象的原型:{name: "jaychou", age: 34, job: "singer", sayName: ƒ, constructor: ƒ}
console.log(Object.getPrototypeOf(person1));
console.log(Object.getPrototypeOf(person1).name);//jaychou

之前也有提及,查询属性和方法时先在当前实例中找,没有的话就到实例的原型链中找,而在实例中添加的属性会屏蔽原型中的同名属性。

  • 更简单的原型语法

上一个例子中每增加一个属性和方法就要敲一遍Person.prototype,比较麻烦,所以更简单的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:

Person.prototype = {
name:'jaychou',
age:34,
job:'singer',
sayName:function () {
console.log(this.name);
}
}
var person3 = new Person();
console.log(Person.prototype.constructor == Person);//false
console.log(Person.prototype.constructor == Object);//true
console.log(person3.constructor == Person);//false
console.log(person3.constructor == Object);//true

上面的代码将 Person.prototype设置为等于一个以对象直接量形式创建的新对象,结果就导致了constructor 属性不再指向 Person 了,因为本质上完全重写了默认的prototype对象,因此constructor 属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向 Person函数。看下面的例子就更清楚了:

function Teacher(){

};
var tea1 = new Teacher();
Person.prototype = tea1;
//true:因为Person.prototype被重写成tea1,tea1的constructor自然指向了Teacher构造函数
console.log(Person.prototype.constructor == Teacher);

所以,如果constrctor的值很重要,可以在上面代码的基础上手动加回去,最好仿照原生的把constructor属性设置成不可枚举的:

Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
console.log(Person.prototype.constructor == Person);//true

注意一个问题: 如果某个实例已经被创建了之后,再直接重写了构造函数的原型对象,那么之前已被创建好的对象内部的原型指针还是指向旧的原型,如果旧实例调用了新原型里面定义的方法,就会报错了。所以重写函数的原型对象时要特别注意这个问题。

  • 原型对象模式的问题

以上的创建对象在原型里共享了所有的属性和方法,对于方法还好,对于大多数情况下属性共享带来的问题就显而易见了。

4.组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor:Person,
sayName:function () {
console.log(this.name);
}
}
var person1 = new Person('jaychou',34,'singer');
var person2 = new Person('xiaoming',15,'student');
person1.sayName();//jaychou
person2.sayName();//xiaoming
console.log(person1.sayName === person2.sayName);//true

5.动态原型模式(最常用)

对上面的例子进行视觉上的美化,希望把所有的内容都放在构造函数里面,可以通过检查某个应该存在的方法或属性是否存在,来决定是否需要初始化原型:

function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job; //添加共享的方法或属性
if(typeof this.sayName != "function"){
Person.prototype.sayName = function () {
console.log(this.name);
};
Person.prototype.sayJob = function () {
console.log(this.job);
}
}
}

注意: 使用动态原型模式时,不能使用对象直接量重写原型,原因上面已经解释过了,重写会切断了旧实例和新原型之间的联系。

js知识梳理3:创建对象的模式探究的更多相关文章

  1. js知识梳理4.继承的模式探究

    写在前面 注:这个系列是本人对js知识的一些梳理,其中不少内容来自书籍:Javascript高级程序设计第三版和JavaScript权威指南第六版,感谢它们的作者和译者.有发现什么问题的,欢迎留言指出 ...

  2. js知识梳理5:关于函数的要点梳理(1)

    写在前面 注:这个系列是本人对js知识的一些梳理,其中不少内容来自书籍:Javascript高级程序设计第三版和JavaScript权威指南第六版,感谢它们的作者和译者.有发现什么问题的,欢迎留言指出 ...

  3. js知识梳理6:关于函数的要点梳理(2)(作用域链和闭包)

    写在前面 注:这个系列是本人对js知识的一些梳理,其中不少内容来自书籍:Javascript高级程序设计第三版和JavaScript权威指南第六版,感谢它们的作者和译者.有发现什么问题的,欢迎留言指出 ...

  4. js知识梳理2:对象属性的操作

    1.属性的查询和设置 ①基本语法 这个简单,可以通过点(.)或方括号([])运算来获取属性的值,注意点运算符后的标识符不能是保留字,方括号内的表达式必须返回字符串或返回一个可以转换成字符串的值. va ...

  5. js知识梳理1:理解对象的属性特性

    1.数据属性 数据属性的4个特性: Configurable:①表示能否通过delete删除属性从而重新定义,②能否修改属性的特性,③能否把属性修改为访问器属性.对象直接量里默认值true. Enum ...

  6. JS知识梳理--图表

  7. Vue.js 2.x API 知识梳理(一) 全局配置

    Vue.js 2.x API 知识梳理(一) 全局配置 Vue.config是一个对象,包含Vue的全局配置.可以在启动应用之前修改指定属性. 这里不是指的@vue/cli的vue.config.js ...

  8. js基础知识梳理(最简版)

    基础的JavaScript知识,只放XMind截图.小白 JS01 JS02 JS03 最基础的js知识--!

  9. js基础梳理-如何理解作用域和作用域链?

    本文重点是要梳理执行上下文的生命周期中的建立作用域链,在此之前,先回顾下关于作用域的一些知识. 1.什么是作用域(scope)? 在<JavaScritp高级程序设计>中并没有找到确切的关 ...

随机推荐

  1. pandas常用操作详解——pd.concat()

    concat函数基本介绍: 功能:基于同一轴将多个数据集合并 pd.concat(objs, axis=0, join='outer', join_axes=None, ignore_index=Fa ...

  2. Java 8 + 10 = Java 18

    明天Java 18将正式发布, 虽然它不是长期支持 (LTS) 版本,但它却实现了九个 JEP(在Java 18列出).有哪些特性值得关注呢?今天胖哥为你提前解读.再看.点赞.转发.关注来一波吧. J ...

  3. 面板Panel

    面板 主要步骤: 1.new一个frame窗口 格式 Frame frame = new Frame() 2.设置窗口的大小.位置.可见性 3.设置frame窗口的布局格式(分为流式布局,东西南北中, ...

  4. Centos7 使用Docker安装rocket.chat聊天工具

    镜像下载.域名解析.时间同步请点击阿里云开源镜像站 下载安装 Rocket.Chat 目前最新的版本为 4.0.1,可以通过手动或者容器的方式安装.这里我推荐使用容器,部署过程会方便不少. 如果要用容 ...

  5. 使用阿里云镜像站NTP服务搭建NTP服务器(基于CentOS 7系统)

    镜像下载.域名解析.时间同步请点击 阿里云开源镜像站 一.NTP服务器介绍 网络时间协议(Network Time Protocol,NTP)服务器,也就是日常所说的NTP服务器,用来提供同步时间服务 ...

  6. Linux安装docker 配置Apache

    镜像下载.域名解析.时间同步请点击 阿里云开源镜像站 要求: 安装docker yum install docker -y 将centos镜像导入云主机 将centos镜像导入docker docke ...

  7. 4月8日 python学习总结 模块与包

    一.包 #官网解释 Packages are a way of structuring Python's module namespace by using "dotted module n ...

  8. 平衡树:为什么Redis内部实现用跳跃表

    摘要:Redis使用跳跃表(skiplist)作为有序集合(zset)的底层实现之一. 本文分享自华为云社区<5分钟了解Redis的内部实现跳跃表(skiplist)>,作者:万猫学社. ...

  9. 多线程笔记(学习尚硅谷java基础教程)

    一.基本概念: 程序: 是为完成特定任务.用某种语言编写的一组指令的集合.即指一段静态的代码,静态对象. 进程: 是程序的一次执行过程,或是正在运行的一个程序.是一个动态的过程:有它自身的产生.存在和 ...

  10. error C4996: 'std::_Copy_impl'

    以下代码段在VS2008编译可以通过,只是会提示不安全: std::vector<unsigned char> fileData ="asdfsfsfsfsdf";// ...