js知识梳理3:创建对象的模式探究

大厂光环
• 阅读 1005

写在前面

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

起因

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

检测对象的类

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

1.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()构造函数的实例。

2.constructor属性

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

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

3.构造函数的名称

//返回对象的类
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);
        }
    }
}

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

点赞
收藏
评论区
推荐文章
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
Karen110 Karen110
3年前
一篇文章带你了解JavaScript作用域
在JavaScript中,对象和函数也是变量。在JavaScript中,作用域是你可以访问的变量、对象和函数的集合。JavaScript有函数作用域:这个作用域在函数内变化。一、本地JavaScript变量一个变量声明在JavaScript函数内部,成为函数的局部变量。局部变量有局部作用域:它们只能在函数中访问。JS://codeherecann
徐小夕 徐小夕
4年前
《javascript高级程序设计》核心知识总结
此文是对js高级程序设计一书难点的总结,也是笔者在看了3遍之后的一些梳理和感想,希望能借此巩固js的基础和对一些核心概念有更深入的了解。摘要js基本的数据类型和关键点变量,作用域和内存问题垃圾回收机制面向对象的程序设计实现类与继承的经典方式BOM和DOM对象DOM扩展与高级API介绍高级编程技巧跨文档消息传递和aja
Stella981 Stella981
3年前
Javascript核心对象
JavaScript的实现包括以下3个部分:1)核心(ECMAScript):描述了JS的语法和基本对象。2)文档对象模型(DOM):处理网页内容的方法和接口3)浏览器对象模型(BOM):与浏览器交互的方法和接口ECMAScript扩展知识:① ECMAScript是一个标准,JS只是它的一个实现,其他实现包括ActionScript。
Stella981 Stella981
3年前
JavaScript的 基本数据类型
第一:Javascript对象是第二:Javascript中第三:Javascript的对象是数据;第四:JavaScript中的对象可以简单理解成"名称:值"对(name:value)。名称(name):"名称"部分是一个JavaScript字符串参考https://www
Stella981 Stella981
3年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Stella981 Stella981
3年前
JavaScript面向对象编程的15种设计模式
在程序设计中有很多实用的设计模式,而其中大部分语言的实现都是基于“类”。在JavaScript中并没有类这种概念,面向对象编程不是基于类,而是基于原型去面向对象编程,JS中的函数属于一等对象,而基于JS中闭包与弱类型等特性,在实现一些设计模式的方式上与众不同。ps:本文之讲述面向对象编程的设计模式策略,JavaScript原型的基础请参考阮一峰面向
Stella981 Stella981
3年前
Javascript 基础知识学习(四)
这里接着前面一篇继续!Javascript中的所有所有事物都是对象:字符串、数值、数组、函数·····每个对象都有自己的属性和方法,JS能够自定义对象:在第七点是相关面向对象的知识点。<script//直接创建对象varpeople{n
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Wesley13 Wesley13
3年前
JS创建对象模式7种方法详解
创建对象的几种模式虽然Object构造函数或者字面量,都可以用来创建对象,但这些方式有明显的缺点:使用同一个接口创建很多对象,会产生大量的代码,于是,工厂模式诞生了1工厂模式工厂模式是广为人知的设计模式,抽象了创建具体对象的过程。在ES6的Class创建类之前,是无法创建类的,开发人员就发明了一种函数,用函数来封
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(