摘要:起因构造函数对象字面量都可以用来创建单个对象,但有明显缺点使用同一个接口创建很多对象,会产生大量的重复代码。组合使用构造函数模式和原型模式创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。
写在前面
注:这个系列是本人对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(/functions*([^()]*)(/)[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);//true5.动态原型模式(最常用)
对上面的例子进行视觉上的美化,希望把所有的内容都放在构造函数里面,可以通过检查某个应该存在的方法或属性是否存在,来决定是否需要初始化原型:
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); } } }
注意: 使用动态原型模式时,不能使用对象直接量重写原型,原因上面已经解释过了,重写会切断了旧实例和新原型之间的联系。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/108370.html
摘要:而且在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。在主要考虑对象而不是自定义类型和构造函数的情况下,这个模式也不错。 写在前面 注:这个系列是本人对js知识的一些梳理,其中不少内容来自书籍:Javascript高级程序设计第三版和JavaScript权威指南第六版,感谢它们的作者和译者。有发现什么问题的,欢迎留言指出。 1.原型链 将原型链作...
摘要:跨域请求详解从繁至简前端掘金什么是为什么要用是的一种使用模式,可用于解决主流浏览器的跨域数据访问的问题。异步编程入门道典型的面试题前端掘金在界中,开发人员的需求量一直居高不下。 jsonp 跨域请求详解——从繁至简 - 前端 - 掘金什么是jsonp?为什么要用jsonp?JSONP(JSON with Padding)是JSON的一种使用模式,可用于解决主流浏览器的跨域数据访问的问题...
摘要:关键字计算为当前执行上下文的属性的值。毫无疑问它将指向了这个前置的对象。构造函数也是同理。严格模式无论调用位置,只取显式给定的上下文绑定的,通过方法传入的第一参数,否则是。其实并不属于特殊规则,是由于各种事件监听定义方式本身造成的。 this 是 JavaScript 中非常重要且使用最广的一个关键字,它的值指向了一个对象的引用。这个引用的结果非常容易引起开发者的误判,所以必须对这个关...
摘要:同理,原型链也是实现继承的主要方式的只是语法糖。原型对象也可能拥有原型,并从中继承方法和属性,一层一层以此类推。利用构造函数小明张三张三小明缺点每次实例化都需要复制一遍函数到实例里面。寄生构造函数模式只有被类出来的才能用。 showImg(https://segmentfault.com/img/bVbo4hv?w=1800&h=1000); 引言 最近又攀登了一下JS三座大山中的第二...
摘要:作用域分类作用域共有两种主要的工作模型。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。词法作用域词法作用域中,又可分为全局作用域,函数作用域和块级作用域。 一篇巩固基础的文章,也可能是一系列的文章,梳理知识的遗漏点,同时也探究很多理所当然的事情背后的原理。 为什么探究基础?因为你不去面试你就不知道基础有多重要,或者是说当你的工作经历没有亮点的时候,基础就是检验你好坏的一项...
阅读 1546·2023-04-26 02:43
阅读 2839·2021-11-11 16:54
阅读 1326·2021-09-23 11:54
阅读 1111·2021-09-23 11:22
阅读 2344·2021-08-23 09:45
阅读 832·2019-08-30 15:54
阅读 3079·2019-08-30 15:53
阅读 3164·2019-08-30 15:53