JavaScript高级(一)
- 一、类(之前ES6学过)
- 1.类的用法
- 2.类的继承
- 2.1 extends关键字
- 2.2 super关键字
- (1)子类属性和父类一致,constructor可以省略
- (2)子类要添加属性,必须要先写super
- (3)super关键字调用父类中的普通函数
- 3.类中this的指向问题
- 二、面向对象tab栏
- 三、原型和原型链
- 1.构造函数中的方法浪费内存的问题
- 2.原型对象
- 3.对象的__proto__属性
- 4.原型中的constructor属性
- 5.原型链
- 6.利用原型对象扩展内置对象的方法
一、类(之前ES6学过)
类
抽取了对象的公共部分,它泛指某一大类
对象
特指某一个通过类实例化出来的东西
1.类的用法
ES6中的class其实就类似于ES5中的构造函数
比如下面两个写法是等价的:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.sum = function () {
return this.x + this.y ;
};
var p = new Point(1, 2);
p.sum();
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
return this.x + this.y ;
}
}
var p = new Point(1, 2);
p.sum();
定义sum()
方法的时候,前面不需要加上function
这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加逗号会报错
。
constructor()
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor()
方法,如果没有显式定义,一个空的constructor()
方法会被默认添加。
class Point {
}
// 等同于
class Point {
constructor() {}
}
2.类的继承
2.1 extends关键字
Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。
class Father {
}
class Son extends Father {
}
2.2 super关键字
1、如果父类和子类属性一致,子类的constructor可以省略
2、子类在继承父类的方法后,可以重写父类的方法,也可以添加其他方法,super必须在子类的this之前调用。
3、其实super也可以理解成改变父类构造函数中的this指向,让this指向子类的实例。
4、所有子类继承的父类的属性都写到super里,子类独有的要在super后添加
(1)子类属性和父类一致,constructor可以省略
如果子类和父类在构造器函数中的属性一致,那么子类中的constructor完全可以省略
class Person {
constructor(name,age) {
this.name = name;
this.age = age;
}
speak() {
console.log(this.name, this.age);
}
}
class Student extends Person {
say() {
console.log(this.name + '11');
}
}
let zzy = new Student('zzy',18);
console.log(zzy.age); //18
console.log(zzy.name); //zzy
zzy.speak(); //zzy 18
zzy.say(); //zzy11
(2)子类要添加属性,必须要先写super
1、子类在继承父类的方法后,可以重写父类的方法,也可以添加其他方法,super必须在子类的this之前调用。
2、其实super也可以理解成改变父类构造函数中的this指向,让this指向子类的实例。
3、所有子类继承的父类的属性都写到super里,子类独有的要在super后添加
4、如果父类和子类属性一致,子类的constructor可以省略
class Person {
constructor(name,age) {
this.name = name;
this.age = age;
}
speak() {
console.log(this.name, this.age);
}
}
class Student extends Person {
constructor(name,age,sex) {
// super调用父类构造函数,也可以理解成让父类中this指向子类的实例
super(name,age); //super表示和父类重复的属性,
this.sex = sex; //添加子类独有的属性
}
//添加子类独有的方法
say() {
console.log(this.name + '666');
}
//重写父类的方法
speak() {
console.log(this); //Student
console.log(this.name, this.age, this.sex);
}
}
let zzy = new Student('zzy',18,'男');
console.log(zzy.name); //zzy super让父类中的this指向子类的实例了
console.log(zzy.sex); //男
zzy.say(); //zzy666
zzy.speak(); //zzy 18 男
(3)super关键字调用父类中的普通函数
通过super.函数名()
调用。
如果子类继承父类,子类和父类中有同名方法,那么子类中的实例调用时优先调用子类中的方法,如果子类没有该方法,才去调用父类的该方法。
class Father {
say() {
return '我是爸爸';
}
}
class Son extends Father {
say() {
console.log(super.say() + '的儿子');
}
}
let son = new Son();
son.say(); //我是爸爸的儿子
//就近原则,如果子类有方法,优先调用子类,子类没有才去调用父类里的
3.类中this的指向问题
永远记住,谁调用函数,this就指向谁。不过下面这个如果我要调用公共方法,用箭头函数怎么解决this的问题?奇怪。其实可以用bind也行, this.btn.onclick = this.sing.bind(this)
。这样调用的话,sing里面的this指向的就是constructor里面的this
<button>点击</button>
let that;
class Father {
constructor(uname, uage) {
that = this;
this.name = uname;
this.age = uage;
// this.sing();
this.btn = document.querySelector('button');
this.btn.onclick = this.sing; //这个this指向的是btn
//谁调用方法,this就指向谁
}
sing() {
console.log(this); //<button>点击</button>
console.log(that.name); //zzy
}
}
let f = new Father('zzy', 18);
f.sing();
二、面向对象tab栏
学完vue再来敲原生js感觉真的是非常麻烦,vue一行就能搞定的事情原生js要写几十行。
这个案例值得学习的地方就是老师很多代码的思路,比如利用逻辑短路去做优化。
可以去github下载:https://github.com/ForMyselfs/tabSwitch-by-nativeJs
var that;
class Tab {
constructor(id) {
that = this;
//获取大的容器
this.main = document.querySelector(id);
//获取添加按钮
this.add = this.main.querySelector('.tabadd');
this.ul = this.main.querySelector('.fisrstnav ul');
this.fsection = this.main.querySelector('.tabscon');
this.init();
}
//获取标题和内容节点(由于更新dom需要重新获取,所以要单独定义个方法)
updateNode() {
this.lis = this.main.querySelectorAll('li');
this.sections = this.main.querySelectorAll('section');
//获取删除按钮
this.remove = this.main.querySelectorAll('.iconfont');
//获取修改元素
this.spans = this.main.querySelectorAll('.fisrstnav li span:first-child');
}
//0.初始化操作,让相关的元素绑定事件
init() {
this.updateNode(); //重新获取更新后的节点
//0.1切换绑定事件
for (let i = 0; i < this.lis.length; i++) {
this.lis[i].index = i; //给每个li添加index属性保存索引值
this.lis[i].onclick = this.toggleTab;
this.remove[i].onclick = this.removeTab; /* 删除绑定事件 */
this.spans[i].ondblclick = this.editTab; //编辑功能
this.sections[i].ondblclick = this.editTab; //编辑功能
}
//0.2添加绑定事件
this.add.onclick = this.addTab;
}
//1.切换
toggleTab() {
//排他思想
that.clearClass();
this.className = 'liactive';
that.sections[this.index].className = 'conactive';
}
clearClass() {
for (let i = 0; i < this.lis.length; i++) {
this.lis[i].className = '';
this.sections[i].className = '';
}
}
//2.添加
addTab() {
that.clearClass();
let random = Math.random();
//2.1创建元素
let li = `<li class="liactive"><span>测试${random}</span><span class="iconfont icon-guanbi"></span></li>`
let section = `<section class="conactive">测试${random}</section>`
//2.2追加到父元素中
that.ul.insertAdjacentHTML('beforeend', li);
that.fsection.insertAdjacentHTML('beforeend', section);
//2.3重新初始化,获取节点并绑定事件(不重新初始化,获取的还是旧DOM)
that.init();
}
//3.删除
removeTab(e) {
e.stopPropagation(); //阻止事件冒泡到li的切换,只删除就行
let index = this.parentNode.index;
that.lis[index].remove();
that.sections[index].remove();
that.init(); //重新获取更新后的DOM,避免出现不必要的问题
//删除之后让它前边那个卡高亮
if(document.querySelector('.liactive')) return;
index--;
that.lis[index] && that.lis[index].click(); //逻辑短路,如果index=-1就不点击
}
//4.修改
editTab() {
let value = this.innerHTML;
// 双击禁止选定文字
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
//双击生成文本框
this.innerHTML = `<input type="text" value=${value} >`;
let input = this.children[0];
input.select();
//离开文本框时把文本框的值给span
input.onblur = function() {
this.parentNode.innerHTML = this.value;
}
input.onkeyup = function(e) {
if(e.key === 'Enter') {
this.blur();
}
}
}
}
new Tab('#tab');
三、原型和原型链
1.构造函数中的方法浪费内存的问题
每次new一个实例,就会单独开辟一个内存空间存放实例中的sing函数,如果我们new一百个实例,就要开辟100个内存空间来存放sing函数,这显然很low
function Star(name, age) {
this.name = name;
this.age = age;
this.sing = function() {
console.log('我会唱歌');
}
}
let ldh = new Star('刘德华','18');
let zxy = new Star('张学友','17');
console.log(ldh.sing === zxy.sing); //false…………
2.原型对象
1、原型对象prototype
可以解决上面的问题,每一个构造函数都有一个prototype
原型对象。
2、一般情况下,公共属性定义在构造函数中,公共方法定义在原型对象上。
3、我们可以把那些不变的方法,直接定义在prototype对象上,这样所有的实例都可以共享这些方法,不需要再另外开辟内存空间。
4、可以看到,写到原型对象上后,ldh.sing === zxy.sing
就true
了
function Star(name, age) {
this.name = name;
this.age = age;
}
Star.prototype.sing = function() {
console.log('sing');
}
let ldh = new Star('刘德华','18');
let zxy = new Star('张学友','17');
console.log(ldh.sing === zxy.sing); //true!!!
3.对象的__proto__属性
对象身上系统会自动添加__proto__属性,这个属性会自动指向构造函数的原型对象
console.log(ldh.__proto__ === Star.prototype) //true
方法的查找规则:
1、首先看实例上有没有sing方法, 如果有就执行。
2、如果实例没有sing方法, 因为有__proto__的存在,就通过__proto__去构造函数的prototype身上去查找sing这个方法。
3、实际开发中不使用__proto__这个属性,它只是提供了一条路线指向prototype
4.原型中的constructor属性
对象的原型(__ proto__)和构造函数的原型对象(prototype)里都有一个属性constructor,我们成为构造函数,因为它指向构造函数本身(无限套娃,原型对象里有constructor,constructor里是构造函数,构造函数又有原型对象,原型对象又有constructor…………)。
有的时候我们需要手动让原型对象重新指向构造函数,比如我们要重写prototype对象
function Star(name, age) {
this.name = name;
this.age = age;
}
Star.prototype = {
constructor: Star, //这句话如果不加,就不知道实例指向谁了
sing: function () {
console.log('sing');
}
}
let ldh = new Star('刘德华', '18');
console.log(ldh.__proto__); //加constructor:{constructor: ƒ, sing: ƒ}
console.log(ldh.__proto__); //不加constructor:{sing: ƒ}
不过这个地方有点没搞懂constructor有什么用。。。目前的理解是可以通过constructor找到当前对象指向谁。
三角关系:
5.原型链
理解下面这个图,原型链你就明白了
console.log(Star.prototype.__proto__ === Object.prototype); //true
console.log(Object.prototype.__proto__); //null
对象成员的查找规则:先从自身找,如果没有 =>
通过__proto__向上找原型对象,如果没有 =>
继续通过__proto__向上找原型对象,一直找到null =>
如果都没就是undefined
console.log(ldh.dj); //undefined
6.利用原型对象扩展内置对象的方法
比如我可以在数组对象中添加一个求和的方法,数组就可以直接调用
Array.prototype.sum = function() {
let sum = 0;
//这里面的this指向的是函数的调用者
for (let i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
}
let arr = [1,2,3,4,5];
console.log(arr.sum()); //15