浅析JavaScript的面向对象编程

209 阅读5分钟

浅析JavaScript的面向对象编程

一.面向对象的简单案列(Tab栏切换)

<main>
        <div class="tabsbox" id="tab">
            <!-- tab 标签 -->
            <nav class="fisrstnav">
                <ul>
                    <li class="liactive"><span>测试1</span><span class="iconfont icon-guanbi"></span></li>
                    <li><span>测试2</span><span class="iconfont icon-guanbi"></span></li>
                    <li><span>测试3</span><span class="iconfont icon-guanbi"></span></li>
                </ul>
                <div class="tabadd">
                    <span>+</span>
                </div>
            </nav>
            <!-- tab 内容 -->
            <div class="tabscon">
                <section class="conactive">测试1</section>
                <section>测试2</section>
                <section>测试3</section>
            </div>
        </div>
    </main>
* {
    margin: 0;
    padding: 0;
}

ul li {
    list-style: none;
}

main {
    width: 960px;
    height: 500px;
    border-radius: 10px;
    margin: 50px auto;
}

main h4 {
    height: 100px;
    line-height: 100px;
    text-align: center;
}

.tabsbox {
    width: 900px;
    margin: 0 auto;
    height: 400px;
    border: 1px solid lightsalmon;
    position: relative;
}

nav ul {
    overflow: hidden;
}

nav ul li {
    float: left;
    width: 100px;
    height: 50px;
    line-height: 50px;
    text-align: center;
    border-right: 1px solid #ccc;
    position: relative;
}

.liactive {
    border-bottom: 2px solid green; 
    z-index: 9;
}

#tab input {
    width: 80%;
    height: 60%;
}

nav ul li span:last-child {
    position: absolute;
    user-select: none;
    font-size: 12px;
    top: -18px;
    right: 0;
    display: inline-block;
    height: 20px;
}

.tabadd {
    position: absolute;
    /* width: 100px; */
    top: 0;
    right: 0;
}

.tabadd span {
    display: block;
    width: 20px;
    height: 20px;
    line-height: 20px;
    text-align: center;
    border: 1px solid #ccc;
    float: right;
    margin: 10px;
    user-select: none;
}

.tabscon {
    width: 100%;
    height: 300px;
    position: absolute;
    padding: 30px;
    top: 50px;
    left: 0px;
    box-sizing: border-box;
    border-top: 1px solid #ccc;
}

.tabscon section,
.tabscon section.conactive {
    display: none;
    width: 100%;
    height: 100%;
}

.tabscon section.conactive {
    display: block;
}

var that;
class Tab{
  constructor(id){
    that = this
    // 这里是不需要更改的节点
    this.main = document.querySelector(id)
    this.add = this.main.querySelector('.tabadd') // 添加按钮
    this.fsection = this.main.querySelector('.tabscon')
    this.ul = this.main.querySelector('ul')
    this.init()
  }
  updateNode(){
    // 这里在添加或者删除的时候需要更新的
    this.lis = this.main.querySelectorAll('li')
    this.sections = this.main.querySelectorAll('section')
    this.remove = this.main.querySelectorAll('.icon-guanbi')
    this.spans = this.main.querySelectorAll('.fisrstnav li span:first-child')
  }
  init(){
    this.updateNode()
    this.add.onclick = this.addTab;
    for(var i=0;i<this.lis.length;i++){
      this.lis[i].index = i 
      this.lis[i].onclick = this.toggleTab;
      this.remove[i].onclick = this.removeTab;
      this.spans[i].ondblclick = this.editTab;
      this.sections[i].ondblclick = this.editTab;
    }
  }
  addTab(){
    that.clearClass()
    var random = Math.random();
    var li = '<li class="liactive"><span>新选项卡</span><span class="iconfont icon-guanbi"></span></li>';
    var section = '<section class="conactive">测试 ' + random + '</section>';
    that.ul.insertAdjacentHTML('beforeend', li);
    // 'beforebegin':元素自身的前面。
    // 'afterbegin':插入元素内部的第一个子节点之前。
    // 'beforeend':插入元素内部的最后一个子节点之后。
    // 'afterend':元素自身的后面。
    that.fsection.insertAdjacentHTML('beforeend', section);
    that.init();
  }
  clearClass(){
    for(var i=0;i<this.lis.length;i++){
      this.lis[i].className = ""
      this.sections[i].className = ""
    }
  }
  toggleTab(){
    // 这里的this指向已经改变成了每个li
    that.clearClass()
    this.className = 'liactive'
    that.sections[this.index].className = 'conactive'
  }
  removeTab(e){
    e.stopPropagation();  // 阻止冒泡 因为父级也有点击事件
    var index = this.parentNode.index  // 看看对应li的索引是多少
    that.lis[index].remove()
    that.sections[index].remove()
    that.init()
    if(document.querySelector('liactive')) return  // 如果删除的不是有激活的项
    if(index===0){
      that.lis[index].click()
      return
    }
    index--
    that.lis[index] && that.lis[index].click() // 手动调用toggleTab
  }
  editTab(){
    var str = this.innerHTML
    window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
    this.innerHTML = '<input type="text"/>' // 在span或section中插入input输入框
    var input = this.children[0]
    input.value = str   // 将原来的值赋值给input输入框
    input.select()
    input.onblur = function(){
      // 注意这里的this指向又变为了input输入框
      this.parentNode.innerHTML = this.value
    }
    input.onkeyup = function(e){
      if(e.keyCode === 13){
        // 回车键
        this.blur()  // 手动调用onblur事件
      }
    }

  }
}
new Tab('#tab')

二.JS是面向对象编程的语言吗?

首先JS是一个基于对象的语言不是真正面向对象的编程语言,在ES6之前没有类的概念,但类是面向对象的核心。虽然ES6引入了类的概念,但是只实现了类的继承,而封装与多态并没有实现。

  • ES6之前是通过构造函数和原型实现面向对象编程
  • ES6通过类实现面向对象编程

三.类

class Star{
      constructor(uname,age){
        this.uname = uname
        this.age = age
      }
    }
    // 利用类创建对象 new
    var p = new Star('zcl',22)

四.构造函数

function Star(uname,age){
      this.uname = uname
      this.age = age
      this.sing = function(){
        console.log('我会唱歌')
      }
    }
var p = new Star('zcl',20)

五.new命令的内部原理

  1. 创建一个空对象,作为将要返回的对象实例
  2. 将这个空的对象的原型对象,指向了构造函数的prototype属性对象
  3. 将这个实例对象的值赋值给函数内部的this
  4. 执行构造函数内部的代码

image.png

六.原型链之__proto__与prototype

6.1 对应名称

  • 原型prototype
  • 原型链__proto__

6.2 从属关系

  • prototype:是一个函数的属性 是一个对象__proto__:是对象的一个属性 是一个对象
  • 对象的__proto__保存着该对象的构造函数的prototype

6.3 prototype与__proto__

function Test(){
  this.a = 1
}
const test = new Test()
console.log(Test.prototype)  // prototype是每个函数的属性
console.log(test.__proto__) // __proto__是每个对象的属性
console.log(Test.prototype === test.__proto__) // 对象的__proto__保存着该对象的构造函数的prototype

又因为Test.prototype也是一个对象所以它也有自己的__proto__

console.log(Test.prototype.__proto__ === Object.prototype)  // true

那么Object.prototype也是一个对象也有其__proto__

console.log(Object.prototype.__proto__)   // null

所以说原型链就是一个以对象为基准 以__proto__为连接的链条 一直到Object.prototype

console.log(Test.__proto__ === Function.prototype)  // true  对象的__proto__等于构造其对象的函数的prototype
console.log(Function._proto_ === Function.prototype)  //true 底层规定的 因为它构造了它本身

特殊的:函数既是Function又是Object

6.4 constructor

每个对象在创建时都会自动拥有一个构造函数属性constructor,指向构造函数的引用.

console.log(test.constructor)  //Test
console.log(test.__proto__.constructor) //Test

constructor属性是继承自原型对象,所以test.constructor可以理解为是顺着__proto__来找到的。

七.继承

7.1 ES6的继承方式

class Father {
      constructor(x,y){
        this.x = x 
        this.y = y
      }
      sum(){
        console.log(this.x+this.y)
      }
      say(){
        console.log('Father')
      }
    }
    class Son extends Father{
      constructor(x,y,z){
        super(x,y)  // 调用父类的构造函数并将参数传递进去
        this.x = x
        this.y = y
        this.z = z // 子类独有的属性
      }
      say(){
        super.say() // 调用父类的普通函数
        console.log('Son')
      }
      subtract(){ // 扩展子类的独有方法
        console.log(this.x-this.y)
      }
    }
    var son = new Son(1,2,3)

ES6是通过extends关键字实现继承

7.2 ES6之前的继承方式

function Father(uname,age){
      this.uname = uname
      this.age = age
    }
   // Son.prototype  = Father.prototype   // 这样做确实实现了子类继承了父类的方法 但是子类的方法也会显示到父类上去
   // 剥离子类原型对象 和 父类原型对象的 指向
    Son.prototype = new Father()  // 这里会改变Son.prototype的指向 因为new Father里的constructor是Father 以及一些方法和属性
    Son.prototype.constructor = Son  // 如果利用了对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
    function Son(uname,age){
      Father.call(this,uname,age)
    }
    Father.prototype.money =  function(){
      console.log(100000)
    }
    Son.prototype.exam = function(){
      console.log('exam')
    }
    var son = new Son('zcl',23)

原型.png

原型1.png ES6之前的继承方式主要是根据call方法实现继承,继承方式的优劣可以参考juejin.cn/post/688858… 这篇掘金博客

7.3 create是不是继承的一种方式

 var a = {
      name:1,
    getX:function(){
    console.log('X');
    }
    };
    var b = Object.create(a);  // 直接将a放到了b的原型链上 这是不是也是一种继承的方式
    b.name = 'zcl'
    console.log(b)
    // name: "zcl"
    // __proto__:
    // getX: ƒ ()
    // name: 1
    // __proto__: Object
    console.log(b.name)  // zcl

create()中的参数作为返回实例对象b的原型对象,在a中定义的属性和方法,都能够被b实例对象继承下来。

八.类的本质

// ES6之前 构造函数+原型实现面向对象编程
    //1.构造函数有原型对象prototype
    //2.构造函数原型对象prototype 里面有constructor 指向构造函数本身
    //3.构造函数可以通过原型对象添加方法
    //4.构造函数创建的实例对象有__proto__ 指向 构造函数的原型对象

    // ES6 通过类 实现面向对象编程
    class Star{

    }
    console.log(typeof Star)  //function 类的本质其实还是一个函数 我们可以理解为类就是构造函数的另一种写法
    console.log(Star.prototype) // 类也有原型对象
    console.log(Star.prototype.constructor)
    Star.prototype.sing = function(){console.log(111)}  // 类可以通过原型对象添加方法
    // 所以ES6中的类就是语法糖

九.涉及的其他问题

9.1 静态成员

Star.sex = '男' // 静态成员 在构造函数本身上添加的成员 只能通过构造函数来访问

9.2 判断一个对象是否有该属性

hasOwnProperty()仅从对象本身去寻找是否有该属性
test.hasOwnProperty("a")
"a" in test  // 是从整个原型链上去找