JS构造函数、对象工厂、原型模式
1.对象创建的3中方法
1.1.对象字面量
1 var obj = { 2 name: "mingzi", 3 work: function () { console.log("working...") }, 4 _age: 18, //下划线开头表示该属性不建议在外部被直接访问,但是任然可以被访问,除非使用类或函数定义 5 //age: 18,使用下划线定义属性后,对象上会默认生成这样一个没有下划线的属性引用,指向同一个值,用来被外部访问 6 get age(){ 7 console.log("get方法被调用"); 8 return this._age; //这里不能是this.age,否则会陷入死循环 9 }, 10 set age(val){ 11 console.log("set方法被调用"); 12 if (val > 100 || val < 0) { 13 throw new Error("invalid value"); 14 } else { 15 this._age = val;//这里也一样,不能是this.age 16 } 17 }, 18 address: { 19 home: "dingxi", 20 office: "xian" 21 } 22 } 23 24 console.log(obj.age); //这里如果写成obj._age,则属性访问不经过get方法 25 obj.age = 20;//同样,这里写obj._age也不经过set方法,但能设置成功 26 console.log(obj.age);
使用get/set的好处就是可以对值进行逻辑判断,这种方法定义的对象并不能实现Java中的private私有化效果。
在对多层嵌套对象的属性级联访问时,要对中间属性判空,除了多个if 判断之外,还可以这么写:
1 var result = obj && obj.address && obj.address.home; //这种方式,只要中途有任何一个地方为null或undefined,则result被赋值为空,否侧赋值为最后一个。
注意:没有这个属性的时候会返回undefined,有属性没值的时候才会返回null。
1.2. var obj = new Object()
与第一种创建的对象是一样的,因为其构造器一样。
构造器结构图:
1.3.Object.defineProperties方式
可以定义属性配置项,不常用
2.对象工厂
加入我们需要创建两个具有相同结构的对象,你可能会这么做:
1 var a = { 2 name: "zhangsan", 3 age: 12 4 } 5 var b = a; 6 console.log(a === b);//true 7 b.age = 13; 8 console.log(a);//{ name: 'zhangsan', age: 13 }
但是,显然这么做并不能创建一个具有相同结构的对象b,因为只是复制了一个栈中的引用,堆中的对象是一样的,修改了b.age也就修改了a.age,并不能达到复制对象的效果。
除非复制代码,但这不是程序员应该做的事,所以我们就可以使用对象工厂:
1 function PersonFactory(name, age) { 2 3 return { 4 name: name, 5 age: age 6 } 7 } 8 9 var p1 = PersonFactory("zz",13); 10 var p2 = PersonFactory("zz",13); 11 console.log(p1 === p2); //false
通过一个工厂方法,就可以创建具有相同结构的不同对象了,工厂方法每次被调用,就相当于“复制”了一份代码,达到了复制对象的效果。
工厂方法的缺点是:
生产出来的对象虽然结构一样,但他们之间是完全独立的个体,并不是像Java一样的父对象与子对象之间的继承关系,每个对象占用一份独立的内存,无法共用。
3.构造函数
虽然JS是一门面向对象语言,但是并没有像Java一样的类(class)的概念
因为面向对象的三大特征是继承、封装、多态,并没有类、接口等
对象是一种运行时的构造,类是一种开发时的构造、类和接口只是一种实现面向对象的手段。
所以不同语言在面向对象的本质上是一样的,但实现方式可能有所不同,为了满足需求,JS也通过构造函数的形式实现了类:
1 //构造函数名默认大写,以作区分,可以看作是一个类 2 function Person(name, age) { 3 //私有的不同属性直接挂载在this上,指向创建的对象 4 this.name = name; 5 this.age = age; 6 } 7 //共有的属性挂载在对象原型上,就像父对象 8 Person.prototype.address = "xian"; 9 10 var p1 = new Person("xx",13); 11 var p2 = new Person("yy",14); 12 console.log(p1);//Person { name: 'xx', age: 13 } 13 console.log(p1.address); //xian 14 console.log(p2.address); //xian
构造方法和工厂方法除了可以设置对象原型的属性外效果是一样的,但后者显然更高端一点。
4.原型模式
4.1.原型的定义
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor
(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。它们与构造函数没有直接的关系。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,
则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜
索。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为 null ,也
只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。
4.2更简单的原型语法
1 function Person(){ 2 3 } 4 Person.prototype = { 5 name : "Nicholas", 6 age : 29, 7 job: "Software Engineer", 8 sayName : function () { 9 alert(this.name); 10 } 11 };
在上面的代码中,我们将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外: constructor 属性不再指向 Person 了,而是指向Object,不过可以再设置回来。
4.3原型的动态性
可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型[[Prototype]] 指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。
4.4原型对象的问题
1 function Person(){ 2 3 } 4 Person.prototype = { 5 constructor: Person, 6 name : "Nicholas", 7 age : 29, 8 job : "Software Engineer", 9 friends : ["Shelby", "Court"], 10 sayName : function () { 11 alert(this.name); 12 } 13 }; 14 var person1 = new Person(); 15 var person2 = new Person(); 16 person1.friends.push("Van"); 17 alert(person1.friends); //"Shelby,Court,Van" 18 alert(person2.friends); //"Shelby,Court,Van" 19 alert(person1.friends === person2.friends); //true
即引用类型的原型属性一般来说是私有的,与原型的共享性冲突。
4.5组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。下面的代码重写了前面的例子。
1 function Person(name, age, job){ 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 this.friends = ["Shelby", "Court"]; 6 } 7 Person.prototype = { 8 constructor : Person, 9 sayName : function(){ 10 alert(this.name); 11 } 12 } 13 14 var person1 = new Person("Nicholas", 29, "Software Engineer"); 15 var person2 = new Person("Greg", 27, "Doctor"); 16 person1.friends.push("Van"); 17 alert(person1.friends); //"Shelby,Count,Van" 18 alert(person2.friends); //"Shelby,Count" 19 alert(person1.friends === person2.friends); //false 20 alert(person1.sayName === person2.sayName); //true
这种构造函数与原型混成的模式,是目前在 ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式,这便是JS的伪类模式。
关于原型对象一节,详见《JavaScript高级程序设计(第三版)》148页,写的巨好。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探