(78)Wangdao.com第十五天_JavaScript 面向对象

面向对象编程(Object Oriented Programming,缩写为 OOP)

是目前主流的编程范式。

是单个实物的抽象,

是一个容器,封装了属性(property)和方法(method),属性是对象的状态,方法是对象的行为(完成某种任务)。

将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。

对象可以复用,通过继承机制还可以定制。

具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

 

  • 构造函数

“类” 就是对象的模板,对象就是 “类” 的实例。

JavaScript 语言的对象体系不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

  • 使用构造函数(constructor)作为对象的模板,描述实例对象的基本结构。。
  • 所谓 ”构造函数”,就是专门用来生成实例对象的函数。
  • 一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。
  • 为了与普通函数区别,构造函数名字的第一个字母通常大写。
  • 特点:
    • 函数体内部使用了 this 关键字,代表所要生成的实例
    • 生成对象的时候,必须使用 new 命令
  • new 命令
    • 通过new命令,让构造函数生成一个实例对象
    • new命令执行时,构造函数内部的this,就代表了新生成的实例对象
    • 使用new命令时,根据需要,构造函数也可以接受参数
      • var Vehicle = function (p) {
            this.price = p;
        };
        
        var v = new Vehicle(500);

        new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。但是为了表示是函数调用,推荐使用括号

      • var Vehicle = function (){
            this.price = 1000;
        };
        
        var v = Vehicle();    // 将构造函数当成普通函数调用,不抱错
        v    // undefined    // 但是变量v变成了undefined
        price    // 1000    // price属性变成了全局变量
        
        
        // 一个解决办法是:
        // 构造函数内部使用严格模式,即第一行加上use strict。
        // 这样的话,一旦忘了使用new命令,直接调用构造函数就会报错。
        function Fubar(foo, bar){
          'use strict';
          this._foo = foo;    // 由于 严格模式中,函数内部的 this 不能指向全局对象,默认等于undefined
          this._bar = bar;
        }
        
        Fubar();    // TypeError: Cannot set property '_foo' of undefined

        // 另一个解决办法:
        // 造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象。
        function Fubar(foo, bar) {
            if (!(this instanceof Fubar)) {
                return new Fubar(foo, bar);
            }
        
            this._foo = foo;
            this._bar = bar;
        }
        
        // 此时不管加不加new命令,都会得到同样的结果 Fubar(1, 2)._foo // 1 (new Fubar(1, 2))._foo // 1

         

  • new 命令的原理

使用new命令时,它后面的函数依次执行下面的步骤

1. 创建一个空对象,作为将要返回的对象实例

2. 将这个空对象的隐式原型对象 __proto__,指向构造函数的 prototype 属性

3. 将这个空对象赋值给函数内部的 this 关键字

4. 开始执行构造函数内部的代码

 

    • 如果构造函数内部有 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象
      • 如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象
      • 因为new命令总是返回一个对象,要么是实例对象,要么是return语句指定的对象
        function getMessage() {
            return 'this is a message';
        }
        
        var msg = new getMessage();
        
        msg    // {}
        typeof msg    // "object"

         

    • new 命令简化的内部流程,可以用下面的代码表示
      • function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
            // 将 arguments 对象转为数组
            var args = [].slice.call(arguments);
          
            // 取出构造函数
            var constructor = args.shift();
            
            // 创建一个空对象,继承构造函数的 prototype 属性
            var context = Object.create(constructor.prototype);
        
            // 执行构造函数
            var result = constructor.apply(context, args);
        
            // 如果返回结果是对象,就直接返回,否则返回 context 对象
            return (typeof result === 'object' && result != null) ? result : context;
        }
        
        // 实例
        var actor = _new(Person, '张三', 28);

         

  • 函数内部可以使用 new.target 属性
    • 如果当前函数是 new 命令调用,new.target 指向当前函数,否则为 undefined
      • function f() {
            console.log(new.target === f);
        }
        
        f();    // false    // new.target 指向 undefined
        new f();    // true    // new.target 指向 当前函数

         

    • 使用这个属性,可以判断函数调用的时候,是否使用new命令
      • function f() {
            if (!new.target) {
                throw new Error('请使用 new 命令调用!');
            }
            // ...
        }
        
        f();     // Uncaught Error: 请使用 new 命令调用!

         

  • Object.create() 创建实例对象
    • 有时拿不到构造函数,只能拿到一个现有的对象
    • 可以使用 Object.create() 方法,生成新的实例对象
      • var Person= {
            name: '张三',
            age: 38,
            greeting: function() {
                console.log('Hi! I\'m ' + this.name + '.');
            }
        };
        
        var person2 = Object.create(Person);
        
        person2.name;    // 张三
        person2.greeting();    // Hi! I'm 张三.

         

 

  • this 关键字

this除了可以在构造函数中表示实例对象,还可以用在其他场合

但是不管什么场合,this 总是返回一个对象

  • this 的指向是可变的
    • 只要函数被赋给另一个变量,this 的指向就会变
      • var A = {
            name: '张三',
            describe: function () {
                return '姓名:'+ this.name;
            }
        };
        
        var name = '李四';    // window.name
        var f = A.describe;    // window.f
        f();    // "姓名:李四"

         

    • 根据据当前所在的对象不同,this 的指向也不同
      • function f() {
            return '姓名:'+ this.name;
        }
        
        var A = {
            name: '张三',
            describe: f
        };
        
        var B = {
            name: '李四',
            describe: f
        };
        
        A.describe();    // "姓名:张三"
        B.describe();    // "姓名:李四"

         

    • 一个网页编程的例子
      • <input type="text" name="age" size=3 onChange="validate(this, 18, 99);">
        
        <script>
        function validate(obj, lowval, hival){
            if ((obj.value < lowval) || (obj.value > hival))
                console.log('Invalid Value!');
        }
        </script>
        // 一个文本输入框,每当用户输入一个值,就会调用onChange回调函数,验证这个值是否在指定范围。
        // 浏览器会向回调函数传入当前对象,因此 this 就代表传入当前对象(即文本框),然后就可以从 obj.value 上面读到用户的输入值

         JavaScript 支持运行环境动态切换,也就是说,this的指向是动态的,没有办法事先确定到底指向哪个对象

  • this 实质
  • var obj = { foo:  5 };
    // 原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。
    // 举例来说,上面例子的 foo 属性,实际上是以下面的形式保存的
    
    {
      foo: {
        [[value]]: 5
        [[writable]]: true
        [[enumerable]]: true
        [[configurable]]: true
      }
    }

     

    • 有 this 的设计,跟内存里面的数据结构有关系
      • 由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行
      • 由于函数可以在    不同的运行环境(上下文)  执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)
      • 所以,this 就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
        • var f = function () {
              console.log(this.x);
          }
          
          var x = 1;
          var obj = {
              ff: f,
              x: 2,
          };
          
          // 单独执行
          f();    // 1
          
          // obj 环境执行
          obj.ff();    // 2

           

    • 几个使用场合下,this 的指向
      • 全局环境
        • 全局环境使用this,它指的就是顶层对象window
        • 不管是不是在函数内部,只要是在全局环境下运行,this 就是指顶层对象window 
      • 构造函数
        • 构造函数中的 this ,指的是实例对象构造函数中的 this ,指的是实例对象
      • 方法调用时
        • this 指向调用该对象的对象。
      • call() 和 apply() 时
        • 指向指定的对象

 

    • 由于 this 的指向是不确定的,所以切勿在函数中包含多层的 this 
      • 使用一个变量固定 this 的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握
      • var o = {
            f1: function () {
                console.log(this);
                var f2 = function () {
                    console.log(this);
                }();
            }
        }
        
        o.f1();    
        // Object
        // Window

         

      • 使用一个变量固定 this 的值,然后内层函数调用这个变量
        var o = {
            f1: function() {
                console.log(this);
        var that = this; var f2 = function() { console.log(that); }(); } } o.f1() // Object // Object


        // 以上代码等价于
        var temp = function () {
        console.log(this);
        };
        var o = {
          f1: function () {
            console.log(this);
            var f2 = temp();
          }
        }
         

         

      • 严格模式下,如果函数内部的 this 指向顶层对象,就会报错。

 

    • 避免数组处理方法中的 this 
      • 数组的mapforeach方法,允许提供一个函数作为参数。这个函数内部不应该使用this
      • 如果有
        • 使用中间变量固定this
        • 将 this 当作 foreach( , this) 方法的第二个参数,固定它的运行环境
        • var o = {
              v: 'hello',
              p: [ 'a1', 'a2' ],
              f: function f() {
                  this.p.forEach(function (item) {
                      console.log(this.v + ' ' + item);
                  });
              }
          }
          
          o.f()
          // undefined a1
          // undefined a2
          
          
          
          // 解决方法1    使用变量固定 this
          var o = {
              v: 'hello',
              p: [ 'a1', 'a2' ],
              f: function f() {
                  var that = this;
                  this.p.forEach(function (item) {
                      console.log(that.v+' '+item);
                  });
              }
          }
          
          o.f()
          // hello a1
          // hello a2
          
          
          
          // 解决方法2    forEach( , this)
          var o = {
              v: 'hello',
              p: [ 'a1', 'a2' ],
              f: function f() {
                  this.p.forEach(function (item) {
                      console.log(this.v + ' ' + item);
                  }, this);
              }
          }
          
          o.f()
          // hello a1
          // hello a2

           

    • 回调函数中的 this 往往会改变指向,最好避免使用
        • var o = new Object();
          o.f = function () {
              console.log(this === o);
          }
          
          // jQuery 的写法
          $('#button').on('click', o.f);

          上面代码中,点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的 DOM 对象,因为f方法是在按钮对象的环境中被调用的。

 

    • 固定 this 指向

JavaScript 提供了callapplybind这三个方法,来切换/固定this的指向

      • Function.prototype.call()
    • 函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数
      • var obj = {};
        
        var f = function () {
            return this;
        };
        
        f() === window    // true
        f.call(obj) === obj    // true

         

    • call 方法的参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象

 

    • 如果 call 方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入 call 方法
      • var f = function () {
            return this;
        };
        
        f.call(5)
        // Number {[[PrimitiveValue]]: 5}

         

    • call 方法还可以接受多个参数    func.call(thisObj, arg1, arg2, ...)
      • 后面的参数是函数调用时所需的参数
      • function add(a, b) {
            return a + b;
        }
        
        add.call(this, 1, 2);    // 3

         

    • 应用 之 调用对象的原生方法
      • var obj = {};
        obj.hasOwnProperty('toString');    // false
        
        
        // 覆盖掉继承的 hasOwnProperty 方法
        obj.hasOwnProperty = function () {
            return true;
        };
        obj.hasOwnProperty('toString');    // true
        // hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果
        
        
        // 将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果
        Object.prototype.hasOwnProperty.call(obj, 'toString');    // false

 

      • Function.prototype.apply()
    • 与 call 方法类似,也是改变 this 指向,然后再调用该函数。
    • 唯一的区别就是,它只有 2 个参数,第二个参数接收一个数组作为函数执行时的参数    func.call(thisObj, [arg1, arg2, ...])
      • 第一个参数也是 this 所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象
    • 很多有趣的应用
      • 找出数组最大元素
        • var a = [10, 2, 4, 15, 9];
          
          // 结合使用apply方法和Math.max方法,就可以返回数组的最大元素
          Math.max.apply(null, a);    // 15

           

      • 将数组的空元素变为 undefined
        • // 利用 Array构造函数 和 apply结合
          // 将数组的空元素变成undefined
          Array.apply(null, ['a', ,'b']);    // [ 'a', undefined, 'b' ]

          空元素与undefined的差别在于,数组的forEach方法会跳过空元素,但是不会跳过undefined

      • 转换类似数组的对象
        • 利用数组对象的slice方法,将一个类似数组的对象(比如arguments对象)转为真正的数组
        • // 前提是
              //被处理的对象必须有length属性
              //以及相对应的数字键
          Array.prototype.slice.apply({0: 1, length: 1});    // [1]
          Array.prototype.slice.apply({0: 1});    // []
          Array.prototype.slice.apply({0: 1, length: 2});    // [1, undefined]
          Array.prototype.slice.apply({length: 1});    // [undefined]

           

      • 绑定回调函数的对象
        • var o = new Object();
          
          o.f = function () {
              console.log(this === o);
          }
          
          var f = function (){
              o.f.apply(o);    // 或者 o.f.call(o);
          };
          
          // jQuery 的写法
          $('#button').on('click', f);
          
          //点击按钮以后,控制台将会显示true。
          // 由于apply方法(或者call方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。
          // 更简洁的写法是采用下面介绍的bind方法。

 

      • Function.prototype.bind()
    • 用于将函数体内的 this 绑定到某个对象,然后返回一个新函数
        • var d = new Date();
          d.getTime() // 1481869925657
          
          var print = d.getTime;
          print() // Uncaught TypeError: this is not a Date object.
          
          // 因为getTime方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了

          bind方法可以解决这个问题

          • var print = d.getTime.bind(d);
            print();    // 1481869925657

            bind 方法将 getTime 方法内部的 this 绑定到 d 对象,这时就可以安全地将这个方法赋值给其他变量了

    • bind 方法的参数就是所要绑定 this 的对象
    • 下面是一个更清晰的例子
      • var counter = {
            count: 0,
            inc: function () {
                this.count++;
            }
        };
        
        var func = counter.inc.bind(counter);
        func();
        counter.count    // 1

        counter.inc() 方法被赋值给变量 func 。这时必须用 bind 方法将 inc 内部的 this,绑定到 counter,否则就会出错

    • this绑定到其他对象也是可以的
      • var counter = {
            count: 0,
            inc: function () {
                this.count++;
            }
        };
        
        var obj = {
            count: 100
        };
        var func = counter.inc.bind(obj);
        func();
        obj.count    // 101

         

    • bind 还可以接受更多的参数,将这些参数绑定原函数的参数
      • var add = function (x, y) {
            return x * this.m + y * this.n;
        }
        
        var obj = {
            m: 2,
            n: 2
        };
        
        var newAdd = add.bind(obj, 5);
        newAdd(5)    // 20

        bind() 方法除了绑定 this 对象,还将 add 函数的第一个参数 x 绑定成 5,然后返回一个新函数 newAdd(),这个函数只要再接受一个参数 y 就能运行了

    • 如果bind() 方法的第一个参数是 null 或 undefined,等于将 this 绑定到全局对象,函数运行时 this 指向顶层对象(浏览器为window)
      • function add(x, y) {
            return x + y;
        }
        
        var plus5 = add.bind(null, 5);
        plus5(10);    // 15

        上面代码中,函数 add 内部并没有 this,使用 bind() 方法的主要目的是绑定参数 x,以后每次运行新函数 plus5,就只需要提供另一个参数 y 就够了。

      • 而且因为 add 内部没有 this,所以 bind() 的第一个参数是 null,不过这里如果是其他对象,也没有影响

 

    • bind() 使用注意
      • 每一次返回一个新函数
        • bind方法每运行一次,就返回一个新函数,这会产生一些问题。
        • 比如,监听事件的时候,不能写成下面这样
          • element.addEventListener('click', o.m.bind(o));
            // 上面代码中,click事件绑定bind方法生成的一个匿名函数。
            
            // 这样会导致无法取消绑定,所以,下面的代码是无效的
            element.removeEventListener('click', o.m.bind(o));
            
            
            
            // 正确写法
            var listener = o.m.bind(o);
            element.addEventListener('click', listener);
            //  ...
            element.removeEventListener('click', listener);

             

      • 结合回调函数使用
        • 回调函数是 JavaScript 最常用的模式之一,
        • 但是一个常见的错误是,将包含 this 的方法直接当作回调函数
          • var counter = {
                count: 0,
                inc: function () {
                    'use strict';
                    this.count++;
                }
            };
            
            function callIt(callback) {
                callback();
            }
            
            // 上面代码中,callIt方法会调用回调函数。
            // 这时如果直接把counter.inc传入,调用时counter.inc内部的this就会指向全局对象。 callIt(counter.inc.bind(counter)); // 使用bind方法将counter.inc绑定counter以后,就不会有这个问题,this总是指向counter counter.count
            // 1

             

        • 某些数组方法可以接受一个函数当作参数。这些函数内部的this指向,很可能也会出错
          • var obj = {
                name: '张三',
                times: [1, 2, 3],
                print: function () {
                    this.times.forEach(function (n) {
                        console.log(this.name);
                    });
                }
            };
            
            obj.print();    // 没有任何输出
            // obj.print内部this.times的this是指向obj的,这个没有问题。
            
            // 但是,forEach方法的回调函数内部的this.name却是指向全局对象,导致没有办法取到值。
            obj.print = function () {
                this.times.forEach(function (n) {
                    console.log(this === window);
                });
            };
            
            obj.print()
            // true
            // true
            // true

            解决方法

            • // 通过bind方法绑定this
              obj.print = function () {
                  this.times.forEach(function (n) {
                      console.log(this.name);
                  }.bind(this));
              };
              
              obj.print()
              // 张三
              // 张三
              // 张三

               

      • 结合call方法使用
        • 利用bind方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的slice方法为例
          • [1, 2, 3].slice(0, 1);    // [1]
            // 等同于
            Array.prototype.slice.call([1, 2, 3], 0, 1);    // [1]

            call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写

            • var slice = Function.prototype.call.bind(Array.prototype.slice);
              slice([1, 2, 3], 0, 1);    // [1]
            • var push = Function.prototype.call.bind(Array.prototype.push);
              var pop = Function.prototype.call.bind(Array.prototype.pop);
              
              var a = [1 ,2 ,3];
              push(a, 4)
              a    // [1, 2, 3, 4]
              
              pop(a)
              a    // [1, 2, 3]

               

        • 如果再进一步,将 Function.prototype.call 方法绑定到 Function.prototype.bind 对象,就意味着 bind 的调用形式也可以被改写
          • function f() {
                console.log(this.v);
            }
            
            var o = { v: 123 };
            var bind = Function.prototype.call.bind(Function.prototype.bind);
            bind(f, o)();    // 123

            所以bind方法就可以直接使用,不需要在函数实例上使用

 

posted @ 2018-10-16 20:48  耶梦加德  阅读(211)  评论(0编辑  收藏  举报