作者: 她不美却常驻我心 博客地址: https://blog.csdn.net/qq_39506551 微信公众号:老王的前端分享 每篇文章纯属个人经验观点,如有错误疏漏欢迎指正。转载请附带作者信息及出处。
工厂函数和构造函数是面向对象编程的基础,由于在 ES6 之前 JS 没有引入类的概念,所以我们只能通过这种模式来实现封装,将对象抽象成类。
首先我们创建一个对象,这很简单:
var human = { name : "小明", sex : "男" }我们很简单的创建了一个对象,但如果需要我们创建十个、一百个是不是会存在大量的重复代码? 工厂模式是软件工程领域一种广为人知的设计模式,目的是为了抽象创建对象,考虑到在 JS 中无法创建类,因此用函数封装以特定接口创建对象。其实现方法非常简单,也就是在函数内创建一个对象,给对象赋予属性及方法再将对象返回即可,而这种函数的写法,被我们成为工厂函数。
function Human(name, sex){ var obj = {}; obj.name = name; obj.sex = sex; return obj; } var human1 = Human("小明","男"); var human2 = Human("小红","女");函数 Human() 能够根据接受的参数来构建一个包含所有必要信息的 Human 对象。可以无数次地调用这个函数,而且每次它都会返回一个独立的对象。 工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为: 函数每次执行都会在内部创建一个对象,然后进行一系列赋值操作之后返回。也就是说调用多少次,就会创建多少个独立的对象。这样创建的对象我们只知道他是一个对象,而并不能像 String、Number 一样分辨出它是那种对象,因为返回的数据类型全部都是 Object。
任何的函数都可以作为构造函数存在,所谓的构造函数只是一个概念上的定义,通过关键字 new 来实例化对象。两者的区别主要是从功能上进行区分的:
构造函数的主要功能是创建特定类型的对象,与 new 一起使用,用于定义 “类”,初始化一个对象:
function Human(name, sex){ this.name = name; this.sex = sex; } var man = new Human("小明","男"); var woman = new Human("小红","女"); man.name; // 小明 woman.name; // 小红两段函数实现了相同的功能,但我们使用构造函数创建出的对象,拥有 constructor 属性,这个属性会指向创建该对象的构造函数,也就是说我们可以通过该属性判断它的对象类型。
console.log(man.constructor === Human); // true console.log(man.constructor === Object); // false但实际上我们一般都会使用 instanceof 运算符来判断对象的类型:
console.log(man instanceof Human); // true console.log(man instanceof Object); // trueinstanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。我们之所以推荐使用它也正是因为它不仅能够判断是否为某个构造函数的实例,还可以判断是否为父构造函数的实例。而 constructor 只能判断出是否为某个构造函数的实例。
当做普通函数调用 构造函数与普通函数的区别只在于调用它们的方式不同,只要是使用关键字 new 来调用,那它就可以被称为是一个构造函数;
Human("小明","男"); window.name; // 小明一个普通的函数在调用时,都是由当前作用域来负责的,而 this 总是指向当前调用它的对象,所以当我们在全局作用域下调用函数时,this 指向的是顶级作用域 window ,此时函数 Human 内的所有对象和方法都挂载到了 window 上。
一个构造函数一般会具有以下特点:
构造函数名首字母大写;所有的属性都赋值给 this,且每个实例都拥有这些属性,但属性是相互独立的,值可以不同;所有的方法都添加给 prototype ,且每个实例都可以访问这些方法;类的方法和静态属性只有构造函数本身可以访问,实例不能访问;必须通过关键字 new 来进行实例化; function Human(name, sex){ // 通过 this 来创建实例属性 this.name = name; this.sex = sex; } // 通过 prototype 在原型链上添加方法 Human.prototype.sayName = function(){ console.log("我的名字是:" + this.name); } // 添加静态属性 Human.age= "我是静态属性 age"; // 添加静态方法 Human.sayAge= function(){ console.log("调用静态方法获取静态属性" + this.age); } // 通过关键字 new 来进行实例化 var man = new Human("小明", "男"); // 通过关键字 new 来创建实例 var woman = new Human("小红","女"); // 每个实例的属性相互独立 man.name; // 小明 woman.name; // 小红 // 每个实例共享方法 man.sayName(); // 我的名字是:小明 woman.sayName(); // 我的名字是:小红 // 静态属性和方法只能由构造函数本身来访问 man.age; // undefined Human.age; // 我是静态属性 age man.sayAge(); // 程序报错: man.sayAge is not a function Human.sayAge(); // 调用静态方法获取静态属性Human我们创建的每个函数都有一个 prototype 原型属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。 也就是说该属性可以让所有对象实例共享它所包含的属性和方法,而之所以将方法添加到原型对象上,是因为如果添加到对象本身十分的消耗内存,我们可以继续往下看。
实际上我们经常需要将某个变量或方法隐藏在构造函数内,只有通过暴露出的方法才能达到访问或修改的目的。类似于 Java 等语言中的 private 将相关内容 “私有化”,让其只能被构造函数的实例方法所访问,且在构造函数的外部是不可见的。
实际上我们通过之前的博客 作用域链 的学习就可以知道,任何在函数中定义的变量,都可以认为是私有变量,因为函数外部不能访问。私有变量包括:函数的参数、局部变量、函数内部定义的其他函数。
这样使构造函数中的 _name 的值会变成不可修改,但 name 属性依然是可写的:
human1.name = function(){ return "小刚"; }JS 内部已经创建好了很多的构造函数,供我们直接使用:
Number()String()Boolean()Object()Array()Function()Date()RegExp()Error()它们为我们提供了很多的基础方法,而这些方法都是添加在原型 prototype 上的,我们可以通过该属性来扩展内置的方法: 例:给 Sring 对象添加一个新方法
String.prototype.replaceAll = function (s1,s2) { return this.replace(new RegExp(s1, "gm"), s2); } var str = "abacaad"; console.log(str.replaceAll("a",""));;我们发现,在添加方法时,我们将方法添加到了函数的原型 prototype 上,而不是与变量一样添加到 this 上。这是因为构造函数每次调用都会创建一个全新的实例化对象,也就是说,如果将方法添加到 this 上,那么每个实例化对象中都会各自拥有一个本质完全一样的同名方法,但不同实例上的同名函数是不相等的:
function Human(){ this.name = name; this.sayName = function(){ console.log(this.name); } } var man1 = new Human("小明"); var man2 = new Human("小红"); console.log(man1.sayName === man2.sayName); // false解决方案也很简单,即将函数转移到构造函数外部:
function Human(){ this.name = name; this.sayName = sayName; } function sayName(){ console.log(this.name); } var man1 = new Human("小明"); var man2 = new Human("小红"); console.log(man1.sayName === man2.sayName); // true这时候,this 上的 sayName 指向的是全局作用域中的函数 sayName,由于本身并不是一个函数,只是一个指向函数的指针,因此 Human 的实例化对象共享了全局作用域中的函数。而函数 sayName 本来应该是只由 Human 的实例化对象调用的局部函数,现在却成了放在全局环境下让任何地方都可以调用的全局函数,所以我们添加方法时都会添加到构造函数的原型prototype 上。
总结一下,就是说构造函数每次调用时都会创建一个新的函数对象,所以多次调用时执行效率会比较低,占用内存。
实际上一旦函数作为构造函数被 new 实例化,会对函数的返回值造成影响,所以通常来说,构造函数都是没有返回值的。 首先我们让函数返回一个基本类型的数据:
fucntion Fn(a){ this.a = a; return a; } var fn1 = new Fn(1); // Fn {a:1} var fn2 = Fn(2); // 2接下来我们修改函数,让其返回一个引用类型的数据:
function Fn(a){ var obj = {}; obj.a = a ; return obj; } var fn1 = new Fn(1); // { a : 1 } var fn2 = Fn(2); // { a : 2 }也就是说:如果返回值为基本数据类型时,构造函数将会返回一个该函数的实例对象,而如果函数返回值为引用数据类型时,则构造函数与普通函数返回的结果相同。
有时候我们希望对象的初始化有多种方式。比如通过元素组成的数组来初始化一个Set对象,而不是通过传入构造函数的参数列表来初始化它:
function Set() { this.values = {}; // 用这个对象的属性保存这个集合 this.n = 0; // 集合中值的个数 // 如果传入一个类数组的对象,将这个元素添加到集合中 // 否则,将所有的参数都添加到集合中 if(arguments.length === 1 && isArrayLike(arguments[0])) { this.add.apply(this, arguments[0]); //把对象利用apply()添加到集合中 }else if(arguments.length > 0) { this.add.apply(this, arguments); // 利用add()方法添加所有参数到集合中 } }通过重载这个构造函数方法让它根据传入参数的不同,来执行不同的初始化方法。这段代码所定义的 Set() 构造函数可以显式将一组元素作为参数列表传入,也可以传入元素组成的数组。但这个构造函数有多义性,如果集合的某个参数是一个数组就将无法通过这个构造函数来创建这个集合了(为了做到这一点,需要首先创建一个空集合,然后显示调用 add() 方法)。
种一棵树,最好的时间是十年前,其次是现在。人的一生,总的来说就是不断学习的一生。 蚕吐丝,蜂酿蜜。人不学,不如物。与其纠结学不学,学了有没有用,不如学了再说。
每篇文章纯属个人经验观点,如有错误疏漏欢迎指正。转载请附带作者信息及出处。您的评论和关注是我更新的动力! 请大家关注我的微信公众号,我会定期更新前端的相关技术文章,欢迎大家前来讨论学习。 都看到这里了,三连一下呗~~~。点个收藏,少个 Bug 。