写在前面

注:这个系列是本人对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. 手写 Vue 系列 之 从 Vue1 升级到 Vue2

    前言 上一篇文章 手写 Vue 系列 之 Vue1.x 带大家从零开始实现了 Vue1 的核心原理,包括如下功能: 数据响应式拦截 普通对象 数组 数据响应式更新 依赖收集 Dep Watcher 编 ...

  2. 如何写好B端产品的技术方案?

    B端产品为企业提供协同办公的工具,帮助企业解决某类经营管理问题,核心价值在于为企业增加收入.降本提效.管控风险,企业级SaaS产品也是B端产品中的一类. B端产品有以下特点: ​客户是一个群体:B端产 ...

  3. SQL学习日记

    目录 SQL学习日记 1. 常见的数据库对象 2. DDL 定义语句 3. DML 操作语句 4. DQL 查询语句 5. DCL 控制语句 SQL学习日记 1. 常见的数据库对象 对象名 关键字 描 ...

  4. .htaccess文件构成的PHP后门

    1..htaccess文件 2.文件上传绕过 一般.htaccess可以用来留后门和针对黑名单绕过 创建一个txt写入(png解析为php) AddType application/x-httpd-p ...

  5. 网络编程-Python的netaddr库

    In [1]: from netaddr import * In [2]: ip = IPAddress('172.16.100.39')   ip.format()ip地址的格式化 '172.16. ...

  6. 输入URL回车之后,究竟发生了什么

    https://blog.csdn.net/androidstarjack/article/details/107031771 在浏览器输入URL回车之后发生了什么?(超详细版)   前言 这个问题已 ...

  7. 简述在 MySQL 数据库中 MyISAM 和 InnoDB 的区别 ?

    MyISAM: 第 134 页 共 485 页不支持事务,但是每次查询都是原子的: 支持表级锁,即每次操作是对整个表加锁: 存储表的总行数: 一个 MYISAM 表有三个文件:索引文件.表结构文件.数 ...

  8. 面试问题之C++语言:volatile关键字的作用

    volatile的作用 volatile关键字是防止在共享的空间发生读取的错误.只保证其可见性,不保证原子性:使用volatile指每次从内存中读取数据,而不是从编译器优化后的缓存中读取数据,简单来讲 ...

  9. Spring通知有哪些类型?

    (1)前置通知(Before advice):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常). (2)返回后通知(After returnin ...

  10. 有哪些不同类型的 IOC(依赖注入)方式?

    构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现 的,该类有一系列参数,每个参数代表一个对其他类的依赖.Setter 方法注入:Setter 方法注入是容器通过调用无参构造器或无参 st ...