对于这个以前面试必考的八股文,在网上有很多批判的声音,很多人说写代码的时候用不到这些,不过我觉得理解了这些概念后对于写代码,特别是理解面向对象编程的思想,还有JavaScript独特的原型这个概念还是非常有帮助的。
所以,对于前端来说,这些概念是必背的。
1、创建对象
1.1 直接创建单个对象
// 对象是属性和方法的集合 // 比如数组,是一个对象,有用length属性,还有几十个方法。 // 狭义的对象,是自己创建的一个小对象。 // 创建一个对象,用来描述一个对象的特征和功能。 /* let arr = [] let arr1 = new Array(); */ // 第一种方法:对象字面量 // 按照键值对的方式存放数据,数据是无序的 let person = { username: '诸葛亮', sex: '男', age: 18, address: '隆中', isMarry: true, friends: ['刘皇叔', '赵云', '关羽', '张飞'], career: '丞相', weight: 140, height: 180, sayHello: function () { console.log(this.username); }, sayFriends: function () { console.log(this.friends); } }; // 第二种:new Object() let person1 = new Object(); person1.username = '关羽'; person1.sex = '男'; person1.weapon = '青龙偃月刀'; person1.beard = '3尺'; person1.face = '红色'; person1.sayHello = function () { console.log(this.username); }
对象字面量可以用来创建单个对象,但是这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。而且效率低下。
1.2 工厂模式
重复创建对象,效率低,那就建立工厂,让函数机器帮忙创建对象,所以通过创建函数来解决这个重复的问题。
其实就是封装了一个函数专门用来创建一个对象,利用传参的方式为对象设置属性和方法,最后再return这个对象。
// 创建很多对象,就需要函数这个机器帮助我们快速生产对象。 // 工厂模式 function workHand(patten, size, work) { let obj = new Object(); obj.patten = patten; obj.size = size; obj.work = work; obj.working = function () { console.log(`这是一张${this.patten}图案的手帕,是${this.work}`); } return obj; } const obj1 = workHand('龙', '大号', '洗脸帕'); obj1.working(); console.log(obj1.patten); console.log(obj1.size); console.log(obj1.work); const obj2 = workHand('凤', '中号', '洗脸帕'); obj2.working(); console.log(obj2.patten); console.log(obj2.size); console.log(obj2.work); // 利用函数来创建对象,虽然效率有所提高,快速创建多个相似的对象,但是不能识别对象到底是哪个函数创建的,就是对象和实例之间的关系 console.log(obj1 instanceof workHand); // false console.log(obj1 instanceof Object); // true // 创建的对象,属性和函数都是单独的,每个对象的方法都是不一样的,导致都要存储在内存中,耗费内存,冗余。 console.log(obj1.working == obj2.working); // false
每个实际生产出来的对象,虽然是根据一个方法来的,但是并不相同。所以每个产生出来的对象并不相同。
通过工厂模式,能够迅速创建想要的对象。简洁、明了、迅速,并且所有的对象的基本结构都是一样的。
工厂模式虽然解决了创建多个相似对象的问题,但是却没有解决对象识别的问题(即怎样知道一个对象的类型,无法用 instanceof 去判断)。
几个对象无法共享某一个特定的属性,对象彼此之间都是不相同的。
所以,还是冗余。
1.3 构造函数
除了JavaScript原生的一批构造函数外(如Object、Number、Array),我们也可以自定义构造函数。
构造函数方式可以在类型实例创建时,同步设置一些必备属性和方法。
基本结构:
function WorkHand(patten, size, work) { this.patten = patten; this.size = size; this.work = work; this.working = function () { console.log(`这是一张${this.patten}图案的手帕,是${this.work}`); } } const obj1 = new WorkHand('鸳鸯', '小号', '扇风'); obj1.working(); const obj2 = new WorkHand('荷花', '小号', '装饰'); obj2.working(); // new 做了什么事情 // 创建了一个空对象实例 // 把该对象实例绑定在this上 // 为这个实例设置属性,并赋值 // 并返回这个实例 // 解决了实例和对象之间的对应关系 console.log(obj1 instanceof WorkHand); //true // 没有解决每个对象的属性是自己的,内存依然占用 console.log(obj1.working == obj2.working); //false
与工厂模式的区别:
1、Person 函数名采用首字母大写,这是定义构造函数的标准语法。
2、没有显式地创建对象。
3、直接将属性和方法赋给了this对象。
4、没有return语句
按照惯例,构造函数都应该以第一个字母大写开头,再以“驼峰式”命名。这种方式借鉴于其他OO语言,也是为了区分其他函数。
构造函数也是函数,this都默认指向window对象,新属性要赋给新实例对象,那就得使用new关键字。
使用new关键字创建对象,会经历如下步骤:
1. 新建一个空对象实例
2. 将构造函数的作用域赋给新对象(这时this都指向了新对象)
3. 执行构造函数中的代码(为这个对象添加属性)
4. 返回新对象
使用构造函数的特点:
构造函数与其它函数的唯一区别,在于调用方式不一样。
任何函数,通过new调用,就可以当成构造函数。而任何函数,不通过new调用,则跟普通函数一样。
优点:
可将实例标识为一种特定类型(比如obj1的类型就是WorkHand,可用instanceof判断)
在 Javascript 中使用“构造器”来实现对象的类型化。构造器就是函数,惯例是首字母大写函数名来代表这是一个构造器,用 new+构造器名字 来创建实例对象,在构造器(及其原型属性对象)内部,this 指代实例对象。
构造函数的缺点:
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建了一遍。working函数在不同的对象实例中,都是不一样的。会导致不同的作用域链和标识符解析。不同实例上的同名函数是不相等的。
每一个类型都拥有的特性,每次在实例级别定义确实有点浪费,那么如果能在类级别定义,每一个实例自动拥有类的通用特征就好了。在这里我们就要用到prototype。
2、原型
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。
JavaScript的所有function 类型的对象都有一个prototype属性。 这个prototype属性用来模拟类,本身又是一个object类型的对象, 因此我们也可以给这个prototype对象添加任意的属性和方法。
2.1 原型属性
在JavaScript中,函数本身也是一个包含了“方法”和“属性”的对象。
比如call()、apply()等。
prototype就是函数下的一个属性。
我们创建的每个函数都有一个 prototype(原型)属性,他指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
function WorkHand(patten, size, work) { // 在构造函数内部,一般放实例自己的属性。 this.patten = patten; this.size = size; this.work = work; } // 把公共的,所有实例都可以访问的,一般是函数,放入portotype这个对象中 // 函数只有一个,但是可以被所有的实例访问,而且函数内部的this,是哪个实例调用这个函数,this就指向哪个实例 WorkHand.prototype.working = function () { console.log(`这是一张${this.patten}图案的手帕,是${this.work}`); } WorkHand.prototype.sayWork = function () { console.log(`我的作用是:${this.work}`); } const obj1 = new WorkHand('鸳鸯', '小号', '扇风'); const obj2 = new WorkHand('荷花', '小号', '装饰'); console.dir(WorkHand); console.log(WorkHand.name); // 构造函数对象的name属性,返回函数名称 console.log(WorkHand.length); //length属性,返回形参的个数 console.log(WorkHand.prototype.constructor); //constructor属性指向构造函数本身 // 通过构造函数的prototype这个属性,这是一个原型属性,返回的是一个对象, // 对象里面有一个constructor属性,这个constructor属性返回的是构造函数本身 obj1.working(); obj2.working(); // 访问的都是同一个函数,所以节约了内存。 console.log(obj2.working == obj1.working); //true // 每一个实例都有一个__proto__属性,指向构造函数的原型对象。 // 每一个实例就可以使用构造函数原型对象里面的属性方法。 console.log(obj1.__proto__); console.log(obj1.__proto__ == WorkHand.prototype); // true obj1.sayWork();
每一个类(构造函数)都具有一个prototype属性,当创建这个类的实例对象时,原型对象的所有属性都被立即赋予要创建的对象中。
2.2 原型操作
设值:
构造函数.原型.属性=属性值
构造函数.原型.方法=函数
取值:
对象实例.属性
对象实例.方法()
2.3 constructor属性
所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。
WorkHand.prototype.constructor指向WorkHand。
这样构造函数和它的原型对象属性之间就建立了对应关系。
2.4 __proto__ 属性
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象。
obj1.__proto__ == WorkHand.prototype // true
2.5 构造函数、原型、实例之间的关系
每个构造函数都有一个原型对象(prototype属性),原型对象包含一个指向构造函数的指针(constructor),实例包含一个指向原型对象的指针(__proto__)。实例与构造函数没有直接关系。
2.6 修改原型对象
如果每一个原型内的方法都单独写,会很麻烦。
function WorkHand(patten,size,work){ // 在构造函数内部,一般放实例自己的属性。 this.patten = patten; this.size = size; this.work = work; } WorkHand.prototype.working = function(){ console.log(`这是一张${this.patten}图案的手帕,是${this.work}`); } WorkHand.prototype.sayWork = function(){ console.log(`我的作用是:${this.work}`); }
// prototype的值如果是重新赋值了一个新对象,则需要把constructor重新指向到该构造函数上,否则它会指向Object构造函数。 WorkHand.prototype = { constructor:WorkHand, working:function(){ console.log(`这是一张${this.patten}图案的手帕,是${this.work}`); }, sayWork:function(){ console.log(`我的作用是:${this.work}`); } }
和前面单独设置属性的唯一区别在于该原型对象的constructor属性不再指向WorkHand了。
这种本质上重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向WorkHand函数。
如果希望原型对象依然指向WorkHand构造函数,可以重新设置constructor属性,这样设置的constructor属性的enumerable为true.
为了避免constructor的enumerable为true,可以使用ES5支持的Object.defineProperty()方法
//设置enumerable为false,指向的对象是WorkHand构造函数 Object.defineProperty(WorkHand.prototype,'constructor',{ enumerable:false, value:WorkHand }) console.log(Object.keys(WorkHand.prototype));
3、扩展内置对象
原型模式的重要性不仅体现在创建自定义类型方面,就连原生的引用类型,都是采用这个模式创建的。
所有原生引用类型(Object、Array、String等)都在其构造函数的原型上定义了方法,比如Array.prototype上定义了sort()方法。
可以对已经存在的内置对象的方法进行重写,也可以扩展没有的方法。
比如重写forEach:
Array.prototype.myForEach = function (callback) { const arr = this; const arg2 = arguments[1] || window; if (typeof callback !== 'function') { throw new Error('第一个参数必须是函数'); } if (arr.length == 0) return; for (let i = 0; i < arr.length; i++) { callback.call(arg2, arr[i], i, arr); } }
Array对象里没有在某个索引位置insert(插入)和remove(删除)两个方法,都是用一个splice方法完成插入、删除等操作。
可以扩展其它的方法:
// 在指定位置插入操作 Array.prototype.insert=function(index, obj){ this.splice(index, 0, obj); } // 在指定位置删除内容 Array.prototype.remove=function(index){ this.splice(index, 1); }
4、继承
在ECMAScript中实现继承主要是依靠原型链来实现的。说是继承,其实我觉得更像"代理"关系。
4.1 原型链
4.1.1概念
基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(__proto__)。
假如让原型对象等于另一个类型的实例,那么此时的原型对象将包含一个指向另一个原型的指针(__proto__),相应的,另一个原型中也包含着一个指向另一个构造函数的指针(constructor)。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。
4.1.2 原型链基本模式
最早期的原型链是用的构造函数的实例成为另一个构造函数的原型来实现的。
// 父类 function SuperType() { this.a = 1; } // 子类 function SubType() { this.b = 2; } //把SuperType的实例作为SubType的原型对象,相当于是重写了SubType构造函数的原型对象。 SubType.prototype = new SuperType(); // 创建子类的实例 const instance = new SubType(); // 访问父类上的属性 console.log(instance.a); // 1 console.log(instance instanceof SuperType); // true console.log(instance instanceof SubType); // true console.log(instance.__proto__.constructor); //SuperType
后来ES5新增了Object.create()方法,Object.create()会创建一个对象并把这个对象的[[Prototype]]关联到指定的对象。
// 父类 const SuperObject = { a: 2 } // 子类 const SubObj = Object.create(SuperObject); // 访问父类的属性 console.log(SubObj.a); // 2 // 子类的原型指针指向父类 console.log(SubObj.__proto__ === SuperObject); // true
可以看看Object.create()是如何工作的:
//polyfill // 如果这个函数不存在 if(!Object.create){ // 创建一个函数 Object.create = function(o){ // 创建一个构造函数 function F(){} // 把构造函数的prototype原型对象指向传递进来的o对象 F.prototype = o; // 返回构造函数的一个实例 return new F(); } }
通过实现原型链,本质上扩展了原型搜索机制。先在实例搜索属性,没有找到,则会继续搜索实例的原型,在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续往上搜索。
在找不到属性和方法的情况下,搜索过程总要一环一环地前行到原型链末端才会停下来。
4.1.3 查找顺序
查找属性和方法的顺序: 对象 > 构造函数 > 原型
先查找实例上的属性,如果没有,再去构造函数里查找,如果没有,再去原型对象里查找。
由于在原型中查找值的过程是一次搜索,因为对原型对象所做的任何修改都能够立即从实例上反映出来——即便是先创建了实例后修改原型对象也照样如此。
// 父类 const SuperObject = { a: 2 } // 子类 const SubObj = Object.create(SuperObject); SubObj.b = 3; // 会遍历出实例继承到的原型链上的属性 for (let attr in SubObj) { console.log(attr); // b a } for (let attr in SubObj) { // 可以通过该方法检测,只输出对象实例自身的属性 if (SubObj.hasOwnProperty(attr)) { console.log(attr); // b } } // 直接使用新方法,返回的是对象实例自身属性的数组 console.log(Object.keys(SubObj)); // ['b']
4.1.4 注意事项
所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype,这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。
5、案例
举一个小案例,鼠标经过产生随机彩色小球。
核心代码:
//构造一个函数 function Ball(x, y, r) { //x,y,r等待传参 this.x = x; this.y = y; this.r = r; //透明度 this.opacity = 0.8; //随机生成散发的位置,如果等于零,则再次循环随机 do { this.dx = Math.floor(Math.random() * 10) - 5; this.dy = Math.floor(Math.random() * 10) - 5; } while (this.dx == 0 && this.dy == 0) //颜色放入数组 var colors = ["#996", "#c1c", "#c63", "#85a", "#19c", "#6cc", "#96c", '#f90', '#ff0', '#09c', '#c06', '#f99', '#9c3', '#6cc', '#9cc' ]; //随机获取颜色数组的下标 this.color = colors[Math.floor(Math.random() * colors.length)]; } //初始化样式 Ball.prototype.init = function () { //生成div放入this.dom中 this.dom = document.createElement("div"); //在#box中插入这个(this.dom)里面 document.getElementById("box").appendChild(this.dom); //当然大家也可以不用把样式也在这里面可以放到css里面然后加一个className //小球样式 //小球定位 this.dom.style.position = "absolute"; //left值等于x轴减去半径 this.dom.style.left = this.x - this.r + "px"; //top值等于y轴减去半径 this.dom.style.top = this.y - this.r + "px"; //width等于半径*2 this.dom.style.width = this.r * 2 + "px"; //height等于半径*2 this.dom.style.height = this.r * 2 + "px"; //backgroundColor等于上面颜色的随机数组 this.dom.style.backgroundColor = this.color; //div方体变圆形 this.dom.style.borderRadius = "50%"; //拿到透明度 this.dom.style.opacity = this.opacity; } //更新移动 Ball.prototype.update = function () { //移动的位置等于x,y加上自己 this.x += this.dx; this.y += this.dy; //更新的时候半径慢慢变小 this.r--; //如果0大于等于更新的半径则执行goDiu()移除; if (this.r <= 0) { this.goDiu() } //只更新半径是没用的,所以我们也要把上面的样式也整体更新一下,不然小球很生硬 this.dom.style.left = this.x - this.r + "px"; this.dom.style.top = this.y - this.r + "px"; this.dom.style.width = this.r * 2 + "px"; this.dom.style.height = this.r * 2 + "px"; } //移除小球 Ball.prototype.goDiu = function () { //删掉元素(this.dom) this.dom.remove(); //for循环进行删除 for (var i = 0; i < BallArr.length - 1; i++) { if (BallArr[i] == this) { BallArr.splice(i, 1); i--; } } } // console.log(this.x); // 每次new一个Ball就放到这个数组里面 var BallArr = []; //创建一个定时器,每20毫秒更新一次 setInterval(function () { //循环BallArr的下标来更新 for (var i = 0; i < BallArr.length; i++) { BallArr[i].update(); } }, 20) //添加鼠标移动DOM操作 document.onmousemove = function (e) { //获取鼠标移动的x轴位置 var x = e.clientX; //获取鼠标移动的y轴位置 var y = e.clientY; //传参x,y,半径; let ball = new Ball(x, y, 30); //初始化 ball.init(); //把当前这个小球放入下面的数组里面 BallArr.push(ball); }
发表评论:
◎请发表你卖萌撒娇或一针见血的评论,严禁小广告。