面向对象是一种编程思想,这样的编程方式很好的划分了各个功能之间明确的分工,类似于现实生活中每个人之间不同的职业划分,这里的每个人就是一个对象,同样JavaScript中也有自己创建对象的方式。
14.01 - 概述
14.1.1 - 面向对象的优点
耦合性低
代码复用
高度模块化
灵活多变,易维护
14.1.2 - 对象的基本构成
一个对象的基本构成一般主要由
属性
和方法
,属性是一个对象的特征,方法是对象的行为。两者之间相互的协调构成了一个完整的对象实例
14.02 - 构造函数
实际上JS中大部分的基本数据类型都是基于面向对象的方式创建的,比如之前学习的,数组,布尔值,字符串,数字等,都有其对应的创建实例化对象的函数,那这样的函数我们一般称之为构造函数。
构造函数,类似于一个对象的模板,包含了此类对象大部分共同的特征
14.2.1 - 基本认识
定义一个构造函数的方法和定义一个普通函数的方式一模一样,只是调用的时候需要用new关键字进行调用,并且构造函数与普通函数之间有一系列不同的特点
构造函数的命名一般为首字母大写
function Person(name){
this.name = name;
}
var pGoudan = new Person('goudan');
console.log(pGoudan.name);// goudan
构造函数需要和
new
关键字一起使用才有意义,否则与普通函数没有区别
function Person(name){
this.name = name;
}
var pGoudan = Person('goudan');
console.log(pGoudan.name);// Uncaught TypeError: Cannot read property 'name' of undefined
构造函数调用时候的内部会发生如下的默认行为
创建一个新的空对象
函数体内的this默认指向这个对象
return这个对象
构造函数内部的new.target属性会指向当前的构造函数
function Test(){
console.log(new.target);
}
new Test();// test
14.03 - 原型
一个构造函数可以构造无数个对象,这些对象拥有很多公共的属性或者方法。原型的作用就是存储这些公共的数据。
实际上原型的大致工作原理是只是开辟一个内存空间,然后无数的对象中共同的方法都指向这个共同的空间。
14.3.1 - 原型基本特征
对象的原型为构造函数的 prototype 属性
function Test(){}
Test.prototype // 构造函数Test的原型
原型指向构造函数新创建的对象
对构造函数添加的属性能被所有实例化对象读取
function Test(){ } Test.prototype.num = 666; var test1 = new Test();// 对象 1 var test2 = new Test();// 对象 2 console.log( test1.num );// 666 console.log( test2.num );// 666
对象属性访问时,构造函数内部this添加的属性,优先级高于原型中的属性
function Test(){ this.num = 888; } Test.prototype.num = 666; var test1 = new Test();// 对象 1 var test2 = new Test();// 对象 2 console.log( test1.num );// 888 console.log( test2.num );// 888
原型中的默认属性
原型中有一些默认属性,其中最重要的是 Fn.prototype.constructor 属性,改属性指向原型所属的构造函数
function Test(){
}
Test.prototype.num = 666;
console.log( Test.prototype.constructor === Test );// true
14.3.2 - 原型比较测试
非原型对比
function Test(){
this.testObj = {};
}
var test1 = new Test();
var test2 = new Test();
console.log(test1.testObj === test2.testObj);// false
原型对比
function Test(){
}
Test.prototype.testObj = {};
var test1 = new Test();
var test2 = new Test();
console.log(test1.testObj === test2.testObj);// true
14.04 - 面向对象相关的方法
实际上所有的对象都继承自 Object ,可以说它是所有对象的起点,也就是所有对象最初始的模板。Object 构造函数下,有许多对象相关的静态方法提供给我们去使用
Object.getPrototypeOf(obj):返回参数对象的原型
Object.create(obj):完全复制参数对象,返回一个新的对象
14.05 - 继承
继承是指让一个对象,继承另外一个对象的所有属性与方法,是常见的需求,继承的特点如下:
子类继承父类的所有属性和方法
子类与父类完全独立,子类的改变不会对父类造成任何影响
14.4.1 - 拷贝继承
function Parent(name){
this.name = name;
}
Parent.prototype.speak = function(){
console.log('My name is ' + this.name);
};
// 复制父类对象的所有属性给子类
function Child(name){
var obj = new Parent(name);
for(var k in obj){
Child.prototype[k] = obj[k];
}
}
// test
var person = new Child('amo');
console.log( person );// Child {}
console.log( person.name );// amo
person.speak();// My name is amo
优点
支持多继承
容易实现
可以传递参数
缺点
拷贝操作相比较消耗内存
无法获取父类不可枚举的属性
父类参数需要一一传递
14.4.2 - 原型继承
// 父类
function Parent(name){
this.name = name;
}
Parent.prototype.speak = function(){
console.log('My name is ' + this.name);
};
// 子类
function Child(){
// 继承父类的自身属性,以及传递参数
Parent.apply(this,arguments);
}
// 继承父类的原型属性
Child.prototype = Object.create(Parent.prototype);
// 修正 constructor 属性的指向
Child.prototype.constructor = Child;
// test
var person = new Child('amo');
console.log( person );// Child {}
console.log( person.name );// amo
person.speak();// My name is amo
优点
可以自动传递参数,不用一一传递
性能消耗较低
可以获取父类的所有属性
缺点
不支持多继承
14.4.3 - 原型链
JavaScript 中的每一个对象都有自己的原型,但同时原型也是一个对象,所以原型也有自己的原型,同样的继续衍生,直到找到最顶层的对象的原型 Object.prototype 这样的关系我们称之为原型链
原型链的意义:
对象继承关系的原理模型
继承的实现
规定继承属性的查找优先级
自身属性的查找优先级大于原型
就近原则
// Object
Object.prototype.num = 0;
// 父类
function Parent(){
this.num = 1;
}
Parent.prototype.num = 2;
// 子类
function Child(){
Parent.call(this);
this.num = 3;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.num = 4;
// 孙类
function Grandson(){
Child.call(this);
this.num = 5;
}
Grandson.prototype = Object.create(Child.prototype);
Grandson.prototype.num = 6;
console.log(new Grandson().num);// 5 3 1 6 4 2 0
14.06 - 链式调用
链式调用是指一个方法执行过后直接”点“上下一个方法继续操作的方式。最典型的的jQuery。这样的操作让代码的执行过程变得清晰,特别适合面向对象的编程思维,实现也很简单
原生方法的链式调用示例
// 数组中的所有值 +1 之后求和
var arr = [1,2,3,4,5];
arr.map(function(item){
return item + 1;
}).reduce(function(a,b){
return a + b;
},0);
自己实现链式调用
// 入口函数
function $mo(select){
this.el = document.querySelectorAll(select);
this.length = this.el.length;
}
// 属性添加方法
$mo.add = function(json){
for(var k in json){
$mo.prototype[k] = json[k];
}
}
// 添加成员
$mo.add({
// 遍历元素
forEach:function(callback){
if(!callback){return;}
for(var i=0;i<this.length;i++){
callback(this.el[i],i);
}
return this;// 把对象返回出去
},
// 添加样式
css:function(attr,val){
this.forEach(function(item){
item.style[attr] = val;
});
return this;// 把对象返回出去
},
// 添加innerHTML内容的方法
html:function(str,replace){
this.forEach(function(item){
item.innerHTML = replace?str:item.innerHTML+str;
});
return this;// 把对象返回出去
}
});
var mo = new $mo("#box");
mo.html('hello world').css('color','red');
14.07 - 包装对象
现在我们知道,只有对象才能进行‘点’读取属性的操作。但是认真思考的话,数字,字符串,布尔值。这些数据实际上都不是一个对象,但是我们也能对它进行‘点’的操作。
原因在于这些数据在进行操作的的时候,会被临时的“包装成一个对象”,从而可以进行“点”的操作。
示例
// 默认方式
var num = 123456;
var _num = num.toString();
console.log( typeof _num + ":" + _num );
// 实际的过程
var num = 123456;
var _num = new Number(num).toString();// 进行包装
console.log( typeof _num + ":" + _num );
包装对象自动销毁机制
包装对象会在对象进行“点”操作的时候默认的对数据进行包装,但是在操作完成之后立即销毁。也就是说进行过两次“点”操作的时候,会生成两个不同的包装对象。
// 试着给包装对象赋值
var num = 123456;
num.testAttr = "hello";// 进行包装
console.log( num.testAttr );//undefined
// 实际的过程
var num = 123456;
new Number(num).testAttr = "hello";// 进行包装 随即销毁
console.log( new Number(num).testAttr );// 新的包装对象不存在 testAttr 属性
14.08 - 原生的构造函数
最后回顾一下,数组方法中把类数组转换成真正的数组的方式之一:
Array.prototype.slice.call('abc')
是不是大概能够猜到这样写的作用:
通过原型得到对应的方法
通过call改变方法内部的this指向。因为默认的方法内部this指向为实例对象
实际上大部分的原生的构造函数中的方法我们都可通过这样的方式从原型中去获取,然后进行使用