JavaScript面向对象知识总结

作者: 一岁一枯荣_ | 来源:发表于2017-02-25 16:04 被阅读193次

    JavaScript对象的理解

    ​ 所谓对象就相当于是一个集合, 集合里面可以有属性 , 函数 , 而每一个属性和函数都有对应的值, 对象里面可以再创建对象 . 对象里面,要用逗号隔开.

    1. 字面量对象

    <script>
      var obj = {
        name:"你好",      // name 是一个属性, 而"你好"  就是属性值   访问的时候用对象名.属性即可(obj.name);
        age:18,
        speak : function(){ // 这里的属性值 是一个方法, 如果要访问,也是一样, 不过既然是方法,后面就要加上() 
          alert("Hello, world"); 
        }
      }
      
      /*给对象添加属性*/
      obj.sex = "男"  // 对象名.属性,  就可以添加属性
      
      //给属性添加方法
      obj.fun = function(){
        alert("这是一个方法");
      }
      
      //给对象修改属性
      obj.name = "Hello";// 直接访问属性, 然后将想要的值修改即可
      
      // 既然可以添加属性, 那当然也可以删除, 这里用到的是一个关键字,delete
      
      delete obj.sex;
    </script>
    

    2.使用for ... in 遍历对象属性

    for (i in objs) {
        alert(i + " " + objs[i]);// 在用for...in遍历的时候, i前面的变量obj指的是属性的名称。
    }
    

    创建对象的几种方式

    1. 使用new Object

    <script>
      var obj = new Object();//这个完全等于 var obj = {}
    
    //这个既然和字面量完全相同, 那么就可以像下面这样操作, 不过 还是认为字面量好一些, 直观.
      //给对象添加属性,
      obj.name = "Hello";
    
     // 添加方法
      obj.speak = function(){
        alert("Hello, world");
      }
    </script>
    

    2.工厂模式创建

    用函数来封装以特定接口创建对象

    <script>
      function createPerson(name, age, job) {
            var o = new Object();
            o.name = name;
            o.age = age;
            o.job = job;
            o.sayName = function() {
                alert(this.name);
            };
            return o;
        }
    var p1 = createPerson("Hello",18,"开发者");
    </script>
    

    3. 构造函数创建

    <script>  
        function Pet(name,age,hobby){  
            this.name=name;//this作用域:当前对象  
            this.age=age;  
            this.hobby=hobby;  
            this.eat=function(){  
                alert("我叫"+this.name+",我今年"+this.age+"岁了,我喜欢"+this.hobby);  
             }  
         }     
      var hehe=new Pet("张三",22,"敲代码");//实例化创建对象  
      hehe.eat(); 
    </script>  
    

    注意

    1. 既然是构造函数, 使用的时候, 就必须用关键字new, 后面跟着构造函数的名,根据需要传入相应的参数
    2. 用构造函数创建对象,经历这几个步骤
      • 创建出来一个新的对象
      • 将构造函数的作用域赋给新对象。意味着这个时候 this就代表了这个新对象
      • 执行构造函数中的代码。 在本例中就是给新对象添加属性,并给属性初始化值。
      • 构造函数执行完毕之后,默认返回新对象。 所以外面就可以拿到这个刚刚创建的新对象了。

    构造函数的特性

    不同于其它的主流编程语言,JavaScript的构造函数并不是作为类的一个特定方法存在的;当任意一个普通函数用于创建一类对象时,它就被称作构造函数,或构造器。一个函数要作为一个真正意义上的构造函数,需要满足下列条件:

    1. 在函数内部对新对象(this)的属性进行设置,通常是添加属性和方法。
    2. 构造函数可以包含返回语句(不推荐),但返回值必须是this,或者其它非对象类型的值。
    3. 为了区别,如果一个函数想作为构造函数,作为国际惯例,最好把这个构造函数的首字母大写。

    JavaScript原型

    原型是Javascript中的继承的继承,JavaScript的继承就是基于原型的继承。

    函数的原型对象

    函数也是一种对象, 他也是属性的集合,我们也可以对函数进行自定义属性。,声明一个函数A的时候, 浏览器就会在内存中创建一个对象B,而且每个函数都默认会有一个属性——prototype。每个函数都有一个属性叫做prototype,  这个prototype的属性值是一个对象(属性的集合),默认的只有一个叫做constructor的属性,指向这个函数本身。
    

    使用构造函数创建对象

    ​ 一个构造函数, 要去使用它的时候, 就需要用关键字new 来创建对象, 而创建对象之后, 会有一个不可见的属性([[proto]])来指向这个构造函数的原型对象(prototype)

    <script type="text/javascript">
            function Obj () {       
            }
            // 访问原型对象,用函数名.prototype即可   Obj.prototype
            //可以给原型对象添加属性,就像给对象添加属性一样, 对象.属性   就可以了
           Obj.prototype.name = "Hello";
            //只有构造函数, 原型对象才有用, 通过new创建
    
            var p1 = new Obj();
            /*创建之后, 可以直接通过 p1.name访问 ,虽然在p1 没有添加name属性, 但是p1中的[[proto]]
            指向Obj的原型里面有name属性.*/
            console.log(p1.name);//Hello
            /* 如果是 p1.name = "World";  它是直接在p1的对象里 创建一个name 属性, 它不会去修改[[proto]]指向的原型对象里的name值, 也没有权利去改变指向的原型对象的值*/
        </script>
    

    与原型有关的属性和方法

    1. prototype属性

      声明一个函数的时候, prototype 就已经自动创建了, 只是在构造函数里, 才会去使用到prototype 属性, 它指向的是构造函数的原型对象

    2. constructor属性

      constructor(构造器): 它指向了构造函数, 也是默认存在原型对象里的, 它可以修改原型对象的指向,

    3. __proto__ 属性

      _proto_ 左右两边各是两个下划线, 用构造函数创建一个新的对象之后, 这个对象默认会有一个不可访问的属性[[proto]], 这个属性指向的构造函数的是原型对象.(chrome 和 火狐浏览器支持访问,IE就不行 )尽量不要去用这个方式去访问

    4. hasOwnProperty() 方法

      判断一个属性是否来自对象本身,

      <script>
       function Person(){
       }
           Person.prototype.name = "张三";
       var p1 = new Person();
       p1.sex = "男";
           //sex属性是直接在p1属性中添加,所以是true
       alert("sex属性是对象本身的:" + p1.hasOwnProperty("sex"));
           // name属性是在原型中添加的,所以是false
       alert("name属性是对象本身的:" + p1.hasOwnProperty("name"));
           //  age 属性不存在,所以也是false
       alert("age属性是存在于对象本身:" + p1.hasOwnProperty("age"));
      </script>
      

      这个方法有个缺陷, 在原型的属性返回false, 不在原型的属性也会返回false, 要判断是否在原型中,我们可以用in

    5. in 操作符

      ​ in操作符用来判断一个属性是否存在于这个对象中。但是在查找这个属性时候,先在对象本身中找,如果对象找不到再去原型中找。换句话说,只要对象和原型中有一个地方存在这个属性,就返回true

      <script type="text/javascript">
       function Person () {
           
       }
       Person.prototype.name = "张三";
       var p1 = new Person();
       p1.sex = "男";
       alert("sex" in p1);     // 对象本身添加的,所以true
       alert("name" in p1);    //原型中存在,所以true
       alert("age" in p1);     //对象和原型中都不存在,所以false
      </script>
      

      我们可以结合这两个来使用, 如果一个属性存在,但是没有在对象本身中,则一定存在于原型中

      <script type="text/javascript">
       function Person () {
       }
       Person.prototype.name = "张三";
       var p1 = new Person();
       p1.sex = "男";
       
       //定义一个函数去判断原型所在的位置
       function propertyLocation(obj, prop){
           if(!(prop in obj)){
               alert(prop + "属性不存在");
           }else if(obj.hasOwnProperty(prop)){
               alert(prop + "属性存在于对象中");
           }else {
               alert(prop + "对象存在于原型中");
           }
       }
       propertyLocation(p1, "age");
       propertyLocation(p1, "name");
       propertyLocation(p1, "sex");
      </script>
      

    使用构造函数模型创建对象的缺陷

    ​ 使用构造函数, 对属性的操作特别适用, 但是对方法的使用,创建多个相同的方法,造成内存增加,性能低下. 我们可以利用各个特性, 去解决这个问题

    使用组合模式解决

    ​ 原型模式适合封装方法,构造方法模式适合封装属性,综合两种模式的优点就有了组合模式。

    <script>
      //构造方法里面封装属性
      function Person(name,age){
        this.name = name;
        this.age  = age ;
      }
    
      //在原型里面封装方法
      Person.prototype.speak = function(speaks){
        alert(this.name + " 说: " + speaks);
      }
      Person.prototype.play = function(playGame){
        alert(this.name + " 爱玩: " + playGame);
      }
      
      var p1 = new Person("Clearlove",22);
      var p2 = new Person("Clearlove7",20);
      p1.speak("4396");
      p1.play("LOL");
      p2.speak("120");
      p2.play("mantis");
    </script>
    

    动态原型模式创建对象

    ​ 前面用到的方法, 也是有瑕疵的, 构造方法和原型 分开来写了, 我们应该把它们放在一块 , 所有有了动态原型模式, 它把所有的属性和方法都封装在构造函数里, 而仅仅在需要的时候才去在构造方法中初始化原型,又保持了同时使用构造函数和原型的优点。

    <script>
      //构造方法里面封装属性
      function Person(name,age){
        this.name = name;
        this.age  = age ;
      /*先判断这个方法是不是function,如果不是function则证明是第一次创建对象,则把这个funcion添加到原型中。如果是function,则代表原型中已经有了这个方法,则不需要再添加。完美解决了性能和代码的封装问题。*/
        if(!Person.prototype.speak){
          Person.prototype.speak = function(){
            alert(this.name + " 说: 4396" )
          }
        }
      }
        var p1 = new Person("Clearlove7");
        p1.speak();
    </script>
    

    注意:

    • 组合模式和动态原型模式是JavaScript中使用比较多的两种创建对象的方式。
    • 建议以后使用动态原型模式。他解决了组合模式的封装不彻底的缺点。

    JavaScript继承

    继承的概念

    ​ 继承是所有的面向对象的语言最重要的特征之一。大部分的oop语言的都支持两种继承:接口继承和实现继承。比如基于类的编程语言Java,对这两种继承都支持。从接口继承抽象方法 (只有方法签名),从类中继承实例方法。

    ​ 但是对JavaScript来说,没有类和接口的概念(ES6之前),所以只支持实现继承,而且继承在 原型链 的基础上实现的。等了解过原型链的概念之后,你会发现继承其实是发生在对象与对象之间。这是与其他编程语言很大的不同。

    原型链的概念

    在JavaScript中,将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法

    ​ 构造函数、原型(对象)和对象之间的关系。每个构造函数都有一个属性 prototype 指向一个原型对象,每个原型对象也有一个属性 constructor 指向函数,通过new 构造函数() 创建出来的对象内部有一个不可见的属性[[proto]]指向构造函数的原型。当每次访问对象的属性和方法的时候,总是先从自身去找,找不到则再去自身指向的原型中找。

    更换构造函数的原型

    ​ 原型对象自身就是一个对象,浏览器就自动的创建了prototype, 我们在定义一个构造函数的时候, 原型对象指向的是这个构造函数. 此时, 我们也可以将原型对象替换成另一个构造函数.

    <script>
      function Father(){
        this.name = "Clearlove7";//直接赋值吧!不转弯了
      }
      Father.prototype.speak = function(){
        alert("我是Clearlove7的方法");
      }
      
      //再来一个构造函数
      function Son(){
        this.age = "18";
      }
      Son.prototype = new Father; // 将Son构造方法的原型替换成Father的对象 因为原型是对象,任何对象都可以作为原型
      Son.prototype.say = function(){
        alert("Clearlove is my father");
      }
      //创建一个Son对象
      var s = new Son()
      //不仅能访问Son的原型对象方法, 也可以访问Father的原型对象方法 -- 这就是继承
      s.speak();
      s.say();
      // 属性也能访问
      console.log(s.name);//Clearlove7
      console.log(s.age);//18
    </script>  
    

    默认顶端原型

    在 JavaScript 中所有的类型如果没有指明继承某个类型,则默认是继承的 Object 类型。这种 默认继承也是通过原型链的方式完成的。

    1. 原型链的顶端一定是Object这个构造函数的原型对象。这也是为什么我们随意创建一个对象,就有很多方法可以调用,其实这些方法都是来自Object的原型对象。
    2. 通过对象访问属性、方法的时候,一定是会通过原型链来查找的,直到原型链的顶端。
    3. 一旦有了继承,就会出现多态的情况。假设需要一个Father类型的数据,那么你给一个Father对象,或Son对象都是没有任何问题的。而在实际执行的过程中,一个方法的具体执行结果,就看在原型链中的查找过程了。给一个实际的Father对象则从Fahter的原型链中查找,给一个实际的Son则从Son的原型链中查找。
    4. 因为继承的存在,Son的对象,也可以看出Father类型的对象和Object类型的对象。 子类型对象可以看出一个特殊的父类型对象

    测试数据的类型方法

    1. typeof: 用来测试对象, 返回的是object
    2. instanceof: 用来测试一个对象是不是属于某个类型。结果为boolean值。
    3. isPrototypeOf( 对象 ) : 这是个 原型对象 的方法,参数传入一个对象,判断参数对象是不是由这个原型派生出来的。 也就是判断这个原型是不是参数对象原型链中的一环。
    <script type="text/javascript">
        function Father () {
            
        }
        function Son () {
            
        }
    
        Son.prototype = new Father();
        var son = new Son();
        alert(Son.prototype.isPrototypeOf(son));  // true
        alert(Father.prototype.isPrototypeOf(son)); // true
        alert(Object.prototype.isPrototypeOf(son)); // true
    </script>
    

    原型链在继承中的缺陷

    1. 共享性问题

      ​ 在原型链中,父类型的构造函数创建的对象,会成为子类型的原型。那么父类型中定义的实例属性,就会成为子类型的原型属性。对子类型来说,这和我们以前说的在原型中定义方法,构造函数中定义属性是违背的。子类型原型(父类型对象)中的属性被所有的子类型的实例所共有,如果有个一个实例去更改,则会很快反应的其他的实例上。

      <script type="text/javascript">
       function Father () {
           this.boys = ["张三", "李四"];
       }
       function Son () {
           
       }
       
       Son.prototype = new Father();   
       //在Son的原型里 就会有一个为boys的属性,
       var son1 = new Son();
       var son2 = new Son();
       //给son1的boys属性的数组添加一个元素
       son1.boys.push("王五");
       //这时,发现son2中的boys属性的数组内容也发生了改变
       alert(son2.boys);  // "张三", "李四", "王五"
      </script>
      
    2. 向父类型的构造函数中传递参数问题

      ​ 在原型链的继承过程中,只有一个地方用到了父类型的构造函数,Son.prototype = new Father();。只能在这个一个位置传递参数,但是这个时候传递的参数,将来对子类型的所有的实例都有效。

      ​ 如果想在创建子类型对象的时候传递参数是没有办法做到的。

      ​ 如果想创建子类对象的时候,传递参数,只能另辟他法。

    借用构造函数调用"继承"

    借用方式(call)

    >借调不是继承,  明显的地方就是**this**, 将一个构造方法内部的 **this** 指向到指定的对象上。
    
    <script type="text/javascript">
        function Father (name,age) {
            this.name = name;
            this.age = age;
        }
        //如果这样直接调用,那么father中的this只的是 window。 因为其实这样调用的: window.father("李四", 20)
        // name 和age 属性就添加到了window属性上
        Father("李四", 20);
        alert("name:" + window.name + "\nage:" + window.age);  //可以正确的输出
    
        //使用call方法调用,则可以改变this的指向
        function Son (name, age, sex) {
            this.sex = sex;
            //调用Father方法(看成普通方法),第一个参数传入一个对象this,则this(Son类型的对象)就成为了Father中的this
            Father.call(this, name, age);
        }
        var son = new Son("张三", 30, "男");
        alert("name:" + son.name + "\nage:" + son.age + "\nsex:" + son.sex);
        alert(son instanceof Father); //false
    </script>
    

    函数借调的方式还有别的实现方式,但是原理都是一样的。但是有一点要记住,这里其实并没有真的继承,仅仅是调用了Father构造函数而已。也就是说,son对象和Father没有任何的关系。

    缺陷:Father的原型对象中的共享属性和方法,Son没有办法获取, 它不是继承

    **但是, 我们可以组合起来使用, 属性用借调, 原型中的方法 我们可以使用继承, 方便了好多

    <script type="text/javascript">
        //定义父类型的构造函数
        function Father (name,age) {
            // 属性放在构造函数内部
            this.name = name;
            this.age = age;
            // 方法定义在原型中
            if((typeof Father.prototype.eat) != "function"){
                Father.prototype.eat = function () {
                    alert(this.name + " 在吃东西");
                }
            }  
        }
        // 定义子类类型的构造函数
        function Son(name, age, sex){
            //借调父类型的构造函数,相当于把父类型中的属性添加到了未来的子类型的对象中
            Father.call(this, name, age);
            this.sex = sex;
        }
        //修改子类型的原型为父类型的对象。这样就可以继承父类型中的方法了。
        Son.prototype = new Father( );
        var son1 = new Son("志玲", 30, "女");
        alert(son1.name);
        alert(son1.sex);
        alert(son1.age);
        son1.eat();
    </script>
    

    注意:

    1. 组合继承是我们实际使用中最常用的一种继承方式。
    2. 可能有个地方有些人会有疑问:Son.prototype = new Father( );这不照样把父类型的属性给放在子类型的原型中了吗,还是会有共享问题呀。但是不要忘记了,我们在子类型的构造函数中借调了父类型的构造函数,也就是说,子类型的原型(也就是Father的对象)中有的属性,都会被子类对象中的属性给覆盖掉。就是这样的。

    作用域和闭包

    变量的作用域

    变量的作用域指的是,变量起作用的范围。也就是能访问到变量的有效范围。

    JavaScript的变量依据作用域的范围可以分为:

    • 全局变量
    • 局部变量

    全局变量

    ==定义在函数外部的变量都是全局变量。==

    全局变量的作用域是==当前文档==,也就是当前文档所有的JavaScript脚本都可以访问到这个变量。

    下面的代码是书写在同一个HTML文档中的2个JavaScript脚本:

    <script type="text/javascript">
        //定义了一个全局变量。那么这个变量在当前html页面的任何的JS脚本部分都可以访问到。
        var v = 20; 
        alert(v); //弹出:20
    </script>
    <script type="text/javascript">
        //因为v是全局变量,所以这里仍然可以访问到。
        alert(v);  //弹出:20
    </script>
    

    再看下面一段代码 :

    <script type="text/javascript">
        alert(a);
        var a = 20;
    </script>
    

    运行这段代码并不会报错, alert(a); 这行代码弹出:undefined。

    为什么在声明 a 之前可以访问变量 a 呢? 能访问 a 为什么输出是undefined而不是20呢?

    ==声明提前!==

    • 所有的全局变量的声明都会提前到JavaScript的前端声明。也就是所有的全局变量都是先声明的,并且早于其他一切代码。
    • 但是变量的赋值的位置并不会变,仍然在原位置赋值。

    所以上面的代码等效下面的代码:

    <script type="text/javascript">
        var a; //声明提前
        alert(a);
        a = 20; //赋值仍然在原来的位置
    </script>
    

    局部变量

    在函数内声明的变量,叫局部变量!表示形参的变量也是局部变量!

    局部变量的作用域是局部变量所在的整个函数的内部。 在函数的外部不能访问局部变量。

    <script type="text/javascript">
        function f(){
            alert(v);  //   弹出:undefined
            var v = "abc";  // 声明局部变量。局部变量也会声明提前到函数的最顶端。
            alert(v);   //  弹出:abc
        }
        alert(v);  //报错。因为变量v没有定义。 方法 f 的外部是不能访问方法内部的局部变量 v 的。
     </script>
    

    全局变量和局部变量的一些细节

    看下面一段代码:

    <script type="text/javascript">
        var m = 10;
        function f(){
            var m = 20;
            alert("方法内部:" + m);  //代码1
        }
        f();
        alert("方法外部:" + m); //代码2
    </script>
    

    在方法内部访问m,访问到的是哪个m呢?局部变量的m还是全局变量的m?

    全局变量和局部变量重名问题

    1. 在上面的代码中,当局部变量与全局变量重名时,局部变量的作用域会覆盖全局变量的作用域。也就是说在函数内部访问重名变量时,访问的是局部变量。==所以 "代码1" 部分输出的是20。==
    2. 当函数返回离开局部变量的作用域后,又回到全局变量的作用域。==所以代码2输出10。==
    3. 如何在函数访问同名的全局变量呢?==通过:window.全局变量名==
    <script type="text/javascript">
        var m = 10;
        function f(){
            var m = 20;
            alert(window.m);  //访问同名的全局变量。其实这个时候相当于在访问window这个对象的属性。
        }
        f();  
    </script>
    

    JavaScript中有没有块级作用域?

    看下面一段代码:

    <script type="text/javascript">
      var m = 5;
      if(m == 5){
        var n = 10;
      }
      alert(n); //代码1
    </script>
    

    代码1输出什么? undefined还是10?还是报错?

    ==输出10!==

    • JavaScript的作用域是按照函数来划分的
    • ==JavaScript没有块级作用域==

    在上面的代码中,变量 n 虽然是在 if 语句内声明的,但是它仍然是全局变量,而不是局部变量。

    **只有定义在方法内部的变量才是局部变量 **

    注意:

    • 即使我们把变量的声明放在 if、for等块级语句内,也会进行==声明提前==的操作!

    闭包

    闭包就是能够读取其他函数内部变量的函数。(个人理解)

    ​ 在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

     function f1(){
        var n=999;
        Add=function(){ //这个匿名函数算是一个闭包
                n+=1
            }
        function f2(){
          alert(n);
        }
        return f2;
      }
      var result=f1();
      result(); // 999   这里调用的时候, 上面的匿名函数没有调用, 直接返回99
        
      Add();  //调用之后,  n的值就变了
      result(); // 1000  
    

    ​ 在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

    ​ 为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

    相关文章

      网友评论

      本文标题:JavaScript面向对象知识总结

      本文链接:https://www.haomeiwen.com/subject/iyqnwttx.html