面向对象
现实世界面向对象:
对象:万事万物都是对象,每个对象都有自己的属性(共有属性,私有属性);
类:抽象对象的特性和功能,形成描述一类事物的抽象概念;在 js 中分为内置类和自定义类;
实例:类中的一个具体的个体。只要是类中的个体就会拥有这个类型全部的属性和特性。
每个人都是人类中的一个实例;
地球上的每个人都是对象,而此时对象属性就是大家都是生活在地球上
按照人类生活的陆地不同,将人类分为5大洲的人,如亚洲人、美洲人、非洲人、欧洲人…. 分类时是抽象了人类生存的陆地不同;
我们每个人就是亚洲人的一个实例,所以我们都有亚洲人的属性,我们都生活在亚洲这块陆地上
JS中的面向对象
面向对象的研究范畴:封装、类的继承和多态(重写、重载)
- 类:js中的类都是一个函数数据类型,都天生自带一个 prototype 属性,它的值是一个对象;
- prototype(原型):每个 prototype 对象都天生自带一个属性 constructor,这个属性的值指向当前类的构造函数本身;
- 对象(实例对象、prototype对象)都有一个 proto** **的属性,这个属性指向当前实例所属类的 prototype。
内置类:Array、String、Number、Function、Date 等
- 内置类的原型:每个类都有自己的原型,存储这个类型公有的属性和方法的
console.log(Array.prototype); // 数组的原型:存储的数组类型公有的属性和方法如果 push、pop、sliceconsole.log(Date.prototype); // Date 的原型:存储 Date 类公有的属性和方法,如 getFullYear、getMonth、getHoursconsole.log(String.prototype); // String 的原型:存储 String 类的公有属性和方法,如 charAt、charCodeAt、toUpperCase 等
- 内置类的实例:
var ary = new Array(1, 3, 5, 7);var date = new Date();ary.push(9);date.getFullYear();
思考?
- 数组实例 ary 怎么找到 push 方法的?
- Date 实例 date 怎么找到 getFullYear 方法的?
自定义类型:通过自定义构造函数
- 定义一个构造函数就是创建一个类
function Teacher(name, age, subject, from = '珠峰') {// 在构造函数中给实例添加私有属性// 构造函数中的 this 指向当前实例this.name = name;this.age = age;this.subject = subject;this.from = from;}
- 给这个类的原型上添加这个类的公有属性
Teacher.prototype.teach = function () {console.log(`${this.name} 老师教 ${this.subject} 课程`);};
- 创建这个类型的实例:
var t1 = new Teacher('马宾', 18, 'js');t1.name// 访问 t1 的私有属性 namet1.age; // 访问 t1 的私有属性 aget1.teach(); // 访问 t1 的公有方法 teach
如何检测私有属性?
- hasOwnProperty() 检测一个属性是否是一个对象的私有属性
console.log(t1.hasOwnProperty('name')); // trueconsole.log(t1.hasOwnProperty('age')); // trueconsole.log(t1.hasOwnProperty('teach')); // false
原型链:属性的查找机制;
- 每个对象(实例对象、普通对象、函数对象),自身都有一个 proto** **属性,这个属性指向所属类的原型
- 当我们 对象.属性名 时,浏览器会现在自己的私有属性找,如果私有属性有这个属性,就使用这个私有属性;如果没有,就根据对象的 proto** 找到所属类的原型(prototype),如果原型上也没有,就通过原型(prototype)对象的 __proto__ **继续向上查找;一直找到基类 Object 的原型对象,如果还没有就返回 undefined;
重写
子类改写父类上的属性或者方法
重载:根据不同的函数签名(函数的参数)自动调用不同的方法;JS 中没有真正意义的重载;
真正意义上的重载:
function sum(int a, int b) {return a + + b;}function sum(char a, char b) {return Number(a) + Number(b)}sum(1, 2)sum('2', '3')
- 但是因为 js 中的同名变量会互相覆盖,同名的函数,这个函数名只能代表最后一个函数,所以不会有真实意义上的重载;
- 但是可以模拟:因为重载要的效果就是根据参数的不同情况作出不同的处理
function sum(a, b) {if (typeof a === 'number' && typeof b === 'number') {return a + b;} else {return Number(a) + Number(b);}}
深入原型、原型链
原型链:对象.属性名 (obj.xxx) 先看私有属性是否有这样是一个属性,如果私有属性中没有,根据实例对象的 proto** 找到当前对象所属类的原型查找,如果原型上也没有,就通过原型对象 __proto__** 继续向上查找; 一直找到 Object.prototype 如果也没有就返回 undefined
function Fn() {this.name = 'Hi';}
1. 原型上添加公有属性的方法:
- 1.1 直接给原型添加方法:
Fn.prototype.say = 'hello';Fn.prototype.title = 'world';Fn.prototype.greeting = function () {console.log('hi~');};
- 1.2 通过实例对象的 proto**
因为实例的 __proto** 指向当前实例所属类的原型对象,所以可以通过修改实例的 proto__ 方式来修改原型对象;
let f1 = new Fn();f1.__proto__.say = 'hello';f1.__proto__.hello = 'world';f1.__proto__.greeting = function () {console.log('hi~')};
- 1.3 修改原型对象的指向:
Fn.prototype = {say: 'hell',hello: 'world',greeting: function () {console.log('hi~')}};console.log(Fn.constructor); // ObjectFn.prototype.constructor = Fn;// 当需要批量给原型增加属性或者方法时,我们需要把一个新的对象赋值给类的原型时,此时要给这个对象增加一个 constructor 属性
用 constructor 检测数据类型:
new Fn().constructor === Fn; // true[].constructor === Array; // true
- 但是有一个问题,constructor 很容易被修改,导致检测结果不准确
Array.prototype.constructor = 123;console.log([1, 2, 3].constructor === Array); // false
面向过程和面向对象比较
- 面向过程
var name = '张三';var age = 26;function eat() {console.log('张三eat')}function drink() {console.log('张三drink')}function sleep() {console.log('张三sleep')}eat();drink();sleep();
- 面向对象
function People(name, age) {this.name = name;this.age = age;}People.prototype.eat = function () {console.log(this.name + 'eat')};People.prototype.drink = function () {console.log(this.name + 'drink')};People.prototype.sleep = function () {console.log(this.name + 'sleep')};// 描述一个主题:let lisi = new People('李四', 20);lisi.eat();lisi.drink();lisi.sleep();let wangwu = new People('王五', 35);wangwu.eat();wangwu.drink();
这样我们发现当我们以后再描述一个人的时候,我们把这个问题划分到解决描述人的问题,需要 People 这个类来完成。只需要 new 一下 People 这个类就可以得到一个对象,而且这个对象自带了 eat、drink、sleep 方法。
面向对象选项卡
- HTML代码
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>选项卡</title><style>* {margin: 0;padding: 0;}ul, li {list-style: none;}.wrapper {margin: 30px auto;width: 606px;height: 400px;border: 1px solid #000;}.header {border: 1px solid #000;}.header li {float: left;width: 200px;height: 40px;line-height: 40px;text-align: center;cursor: pointer;-webkit-user-select: none; /*禁止用户选中*/}.header li:nth-child(2) {border-left: 1px solid #000;border-right: 1px solid #000;}li.active {background: yellow;}.clearfix:after {display: block;content: '';visibility: hidden;clear: both;}.wrapper div {text-align: center;height: 356px;line-height: 356px;display: none;}div.active {display: block;}</style></head><body><div class="wrapper" id="tab1"><ul class="header clearfix" id="header"><li class="active">视频</li><li>秒拍</li><li>综艺</li></ul><div class="active">程序员惨遭亲妈拍卖</div><div>预防脱发秘籍(黑芝麻糊)</div><div>蔡徐坤-我们很爷们儿</div></div><div class="wrapper" id="tab2"><ul class="header clearfix"><li class="active">视频2</li><li>秒拍2</li><li>综艺3</li></ul><div class="active">程序员惨遭亲妈拍卖2</div><div>预防脱发秘籍(黑芝麻糊)2</div><div>蔡徐坤-我们很爷们儿2</div></div><script src="js/5-选项卡插件.js"></script></body></html>
- JS代码
// 1. 创建选项卡类function Tab(options) {// 1. 参数合法校验if (!options || !options.el) {console.error('缺少el元素');return;}// 2. 将传进来的参数对象保存到实例上this._options = options;// 3. 执行获取元素this.queryEle();// 4. 执行绑定事件this.bindEvent();}// 为 Tab 增加公用的获取元素的方法Tab.prototype.queryEle = function () {// 1. 从 this 中的 options 中的 el 获取最外层元素const container = document.querySelector(this._options.el);// 2. 获取选项卡头,并挂载到实例上this.headerList = container.querySelectorAll('.header > li');// 3. 获取所有的卡片并挂载到实例上this.cardList = container.querySelectorAll('div');};// 为 Tab 增加公用的绑定事件的元素Tab.prototype.bindEvent = function () {const HEADER_LIST = this.headerList; // 用一个常量缓存 headerList// 变量 headerList 给每个 li 绑定点击事件for (let i = 0; i < HEADER_LIST.length; i++) {HEADER_LIST[i].onclick = () => {// 这里使用箭头函数,是因为我们希望这里的 this 是 Tab 的实例,如果不使用箭头函数,点击事件函数中的 this 就是选项卡头了this.clearClass();this.addClass(i);}}};// 为 Tab 类增加移除类名的方法Tab.prototype.clearClass = function () {const HEADER_LIST = this.headerList;const CARD_LIST = this.cardList;for (let i = 0; i < HEADER_LIST.length; i++) {HEADER_LIST[i].className = '';CARD_LIST[i].className = '';}};// 为 Tab 类添加类名的方法Tab.prototype.addClass = function (index) {this.headerList[index].className = 'active';this.cardList[index].className = 'active';};new Tab({el: '#tab1'});new Tab({el: '#tab2'});
选项卡插件封装
- 插件分装需要简化构造函数
- 确保是由new操作符调用
// 1. 创建选项卡类function Tab(options) {// 1. 确保如何是通过 new 操作调用if (!(this instanceof Tab)) {console.error('Tab is a constructor which should be call with new');return;}// 2. 参数合法校验if (!options || !options.el) {console.error('缺少el元素');return;}// 3. 将传进来的参数对象保存到实例上this._options = options;// 4. 执行初始化this.init();}// 为 tab 增加 init 方法:Tab.prototype.init = function () {this.queryEle();this.bindEvent();};// 为 Tab 增加公用的获取元素的方法Tab.prototype.queryEle = function () {// 1. 从 this 中的 options 中的 el 获取最外层元素const container = document.querySelector(this._options.el);// 2. 获取选项卡头,并挂载到实例上this.headerList = container.querySelectorAll('.header > li');// 3. 获取所有的卡片并挂载到实例上this.cardList = container.querySelectorAll('div');};// 为Tab增加公用的绑定事件的元素Tab.prototype.bindEvent = function () {const HEADER_LIST = this.headerList; // 用一个常量缓存 headerList// 变量 headerList 给每个 li 绑定点击事件for (let i = 0; i < HEADER_LIST.length; i++) {HEADER_LIST[i].onclick = () => {// 这里使用箭头函数,是因为我们希望这里的 this 是 Tab 的实例,如果不使用箭头函数,点击事件函数中的 this 就是选项卡头了this.clearClass();this.addClass(i);}}};// 为 Tab 类增加移除类名的方法Tab.prototype.clearClass = function () {const HEADER_LIST = this.headerList;const CARD_LIST = this.cardList;for (let i = 0; i < HEADER_LIST.length; i++) {HEADER_LIST[i].className = '';CARD_LIST[i].className = '';}};// 为 Tab 类添加类名的方法Tab.prototype.addClass = function (index) {this.headerList[index].className = 'active';this.cardList[index].className = 'active';};new Tab({el: '#tab1'});new Tab({el: '#tab2'});
