zoukankan      html  css  js  c++  java
  • 《JavaScript语言入门教程》记录整理:面向对象

    本系列基于阮一峰老师的《JavaScrip语言入门教程》或《JavaScript教程》记录整理,教程采用知识共享 署名-相同方式共享 3.0协议。这几乎是学习js最好的教程之一(去掉之一都不过分)

    最好的教程而阮一峰老师又采用开源方式共享出来,之所以重新记录一遍,一是强迫自己重新认真读一遍学一遍;二是对其中知识点有个自己的记录,加深自己的理解;三是感谢这么好的教程,希望更多人阅读了解

    实例对象与 new 命令

    1. 面向对象编程(Object Oriented ProgrammingOOP)将现实世界中的实物、逻辑操作及各种复杂关系抽象为一个个对象,每一个对象完成一定的功能,用来接受信息、处理数据或执行操作、发布信息等,通过继承还能实现复用和功能扩展。比起由一系列函数或指令组成的传统的过程式编程(procedural programming)更适合大型项目。

    2. 什么是"对象"(object):(1)对象是单个实物的抽象。(2)对象是一个容器,封装了属性(property)和方法(method)。属性是对象的状态,方法是对象的行为(完成某种任务)。

    3. 生成对象时,通常需要一个模板,表示某一类实物的共同特征,然后根据模板生成。在C++、java、c#等语言中都有类(class)的概念。"类"就是对象的模板,对象是"类"的实例(即类的一个具体对象)。JavaScript的对象体系基于构造函数(constructor)和原型链(prototype)构成。

    4. JavaScript 语言中构造函数(constructor)就是对象的模板,描述实例对象的基本结构。"构造函数"就是专门用来生成实例对象的函数。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

    5. 构造函数和普通函数一样,但是有自己的特征和用法。

    如下,Vehicle就是构造函数。通常构造函数名字第一个字母大写(与普通函数作区分)。

    var Vehicle = function () {
      this.price = 1000;
    };
    

    构造函数的特点

    • 函数体内部使用了this关键字,代表了所要生成的对象实例。
    • 生成对象的时候,必须使用new命令。
    1. new命令的作用是执行构造函数,返回一个实例对象。
    var Vehicle = function () {
      this.price = 1000;
    };
    
    var v = new Vehicle();
    v.price // 1000
    

    如果忘记了new命令,就成了构造函数作为普通函数直接调用

    为了保证构造函数必须使用new命令,解决办法有两种:

    一、可以在构造函数内部使用严格模式。这样不使用new命令直接调用就会报错

    var Vehicle = function () {
      'use strict';
      this.price = 1000;
    };
    
    var v = Vehicle();  // Uncaught TypeError: Cannot set property 'price' of undefined
    

    严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错

    二、在构造函数内部判断是否使用new命令,如果没有,则根据参数返回一个实例对象。

    function Vehicle(price) {
      if (!(this instanceof Vehicle)) {
        return new Vehicle(price);
      }
    
      this.price = price||1000;
    };
    
    var v1 = Vehicle();
    var v2 = new Vehicle();
    
    1. 使用new命令时,后面的函数依次执行下面的步骤。
    • 创建一个空对象,作为将要返回的对象实例。
    • 将这个空对象的原型,指向构造函数的prototype属性。
    • 将这个空对象赋值给函数内部的this关键字。
    • 开始执行构造函数内部的代码。

    构造函数内部,this指的是一个新生成的空对象。构造函数的目的就是操作一个空对象(即this对象),将其"构造"为需要的样子。

    如果构造函数内部有return语句且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

    var Vehicle = function () {
      this.price = 1000;
      return 1000;  // 忽略非对象的return语句
    };
    
    (new Vehicle()) === 1000
    

    如果return返回的是其他对象而不是this,那么new命令将会返回这个新对象

    如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象。

    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);
    
    1. 函数内部的new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined
    function f() {
      console.log(new.target === f);
    }
    
    f() // false
    new f() // true
    

    此属性可判断是否使用new命令调用了函数

    function f() {
      if (!new.target) {
        throw new Error('请使用 new 命令调用!');
      }
      // ...
    }
    
    f() // Uncaught Error: 请使用 new 命令调用!
    
    1. Object.create() 创建实例对象

    通常使用构造函数作为生成实例对象的模板。但是如果没有构造函数只有对象时,可以使用Object.create()方法以一个对象作为模板,生成新的实例对象。

    如下,对象person1person2的模板,后者继承了前者的属性和方法。

    var person1 = {
      name: '张三',
      age: 38,
      greeting: function() {
        console.log('你好,我是' + this.name + '。');
      }
    };
    
    var person2 = Object.create(person1);
    person2.name;        // "张三"
    person2.name="李四"  // "李四"
    person2.greeting()   // 你好,我是李四。
    
    person1.greeting()  // 你好,我是张三。
    

    this关键字

    1. this关键字总是返回一个对象,或指向一个对象。
    2. this就是属性或方法"当前"所在的对象。也就是说,如果改变属性或方法所在的对象,就可以改变this的指向

    将对象的属性赋给另一个对象,改变属性所在对象,可以改变this的指向。

    如下,通过改变函数f所在的对象,实现this的改变

    function f() {
      return '姓名:'+ this.name;
    }
    
    var A = {
      name: '张三',
      describe: f
    };
    
    var B = {
      name: '李四',
      describe: f
    };
    
    f()          // "姓名:"
    A.describe() // "姓名:张三"
    B.describe() // "姓名:李四"
    

    只要函数被赋给另一个变量,this的指向就会变。

    1. JavaScript中,一切皆对象。运行环境也是对象(顶层函数中,this指向window对象),函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。同时this的指向是动态的

    2. this的本质或this的设计目的:

    js的对象在内存的结构是这样的,对象存在堆中,当把对象赋值给一个变量时,实际是将对象在堆中的内存地址赋值给变量。如下,将对象的地址(reference)赋值给变量obj

    var obj = { foo:  5 };
    

    读取obj.foo的过程是,先从obj拿到内存地址,然后从该地址读出原始的对象,返回它的foo属性

    原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。比如上面的属性foo实际保存形式如下,foo属性的值保存在属性描述对象的value属性里面:

    {
      foo: {
        [[value]]: 5
        [[writable]]: true
        [[enumerable]]: true
        [[configurable]]: true
      }
    }
    

    当属性的值是函数时

    var obj = { foo: function () {} };
    

    js将函数单独保存在内存中,将函数的地址赋值给foo属性的value属性。

    {
      foo: {
        [[value]]: 函数的地址
        ...
      }
    }
    

    因为函数是单独存在的值,所以可以在不同的环境(上下文)执行

    JavaScript允许在函数体内部,引用当前环境的其他变量。

    如下,函数体使用的变量x由运行环境提供。

    var f = function () {
      console.log(x);
    };
    

    由于函数可以在不同的运行环境执行,所以需要一种机制,可以在函数体内部获得当前的运行环境(context)。所以this就被用来设计为,在函数体内部,指代函数当前的运行环境

    如下,函数体中this.x就指当前运行环境的x

    var f = function () {
      console.log(this.x);
    }
    
    1. this的使用场合
    • 全局环境使用this,指的是顶层对象window
    • 构造函数中的this,指的是实例对象。
    • 对象的方法里面包含thisthis的指向就是方法运行时所在的对象。该方法赋值给另一个对象,会改变this的指向。

    关于this的指向并不好把握,比如下面的例子

    var obj ={
      foo: function () {
        console.log(this);
      }
    };
    
    obj.foo() // obj
    

    如上,通过调用boj对象的foo方法,输出this为当前的obj对象。但是,如果使用下面的形式,都会改变this的指向

    // 情况一
    (obj.foo = obj.foo)() // window
    // 情况二
    (false || obj.foo)() // window
    // 情况三
    (1, obj.foo)() // window
    

    上面代码中,obj.foo是获取出来之后再调用,相当于一个值,这个值在调用的时候,运行环境已经从obj变为了全局环境,this的指向变为了window

    可以这样理解,在js引擎内部,obj对象和obj.foo函数储存在两个内存地址,称为地址一和地址二。obj.foo()调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。上面三种情况,都是直接取出地址二进行调用(即取出函数调用),这样的话,运行环境就是全局环境,this指向的是全局环境。上面三种情况等同于下面的代码:

    // 情况一
    (obj.foo = function () {
      console.log(this);
    })()
    // 等同于
    (function () {
      console.log(this);
    })()
    
    // 情况二
    (false || function () {
      console.log(this);
    })()
    
    // 情况三
    (1, function () {
      console.log(this);
    })()
    

    this所在的方法不在对象的第一层时,这时this指向当前一层的对象(即当前所在的对象),而不会继承更上面的层。

    var a = {
      p: 'Hello',
      b: {
        m: function() {
          console.log(this.p);
        }
      }
    };
    
    a.b.m() // undefined
    
    1. this使用中注意点:
    • 避免多层this。用于this的指向可变,尽量不要在函数中包含多层this

    通过添加指向this的变量,实现多层this的使用

    var o = {
      f1: function() {
        console.log(this);
        var that = this;
        var f2 = function() {
          console.log(that);
        }();
      }
    }
    
    o.f1()
    // Object
    // Object
    

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

    • 避免使用数组处理方法(mapforeach方法中的参数函数)中的this

    mapforeach方法的回调函数中的this指向window对象。解决办法是使用一个中间变量固定this,或者使用this作为mapforeach方法的第二个参数

    // 中间变量
    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
    
    // 第二个参数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(往往会改变指向)。
    1. this的动态切换,既体现了灵活,又使编程变得困难和模糊。js提供了callapplybind方法,来切换/固定this的指向。
    2. Function.prototype.call()函数实例call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在指定的作用域中调用该函数

    如下,使用call改变作用域6

    var obj = {};
    
    var f = function () {
      return this;
    };
    
    f() === window // true
    f.call(obj) === obj // true
    

    call方法的第一个参数,应该是一个对象。如果参数为空、nullundefined,则this指向全局对象。

    var n = 123;
    var obj = { n: 456 };
    
    function a() {
      console.log(this.n);
    }
    
    a.call() // 123
    a.call(null) // 123
    a.call(undefined) // 123
    a.call(window) // 123
    a.call(obj) // 456
    

    call方法的第一个参数是一个原始值,则原始值会自动转成对应的包装对象,然后传入call方法。

    var f = function () {
      return this;
    };
    
    f.call(5)   // Number {[[PrimitiveValue]]: 5}
    

    call方法除第一个参数表示调用函数的作用域,其他参数以列表的形式传递,表示函数执行时的参数

    func.call(thisValue, arg1, arg2, ...)
    

    call方法的一个应用是调用对象的原生方法。

    var obj = {};
    obj.hasOwnProperty('toString') // false
    
    // 覆盖掉继承的 hasOwnProperty 方法
    obj.hasOwnProperty = function () {
      return true;
    };
    obj.hasOwnProperty('toString') // true
    
    Object.prototype.hasOwnProperty.call(obj, 'toString') // false
    
    1. Function.prototype.apply()apply方法的作用,也是改变this指向,然后再调用该函数。但是它接收的是一个数组作为函数执行时的参数,
    func.apply(thisValue, [arg1, arg2, ...])
    

    call一样,第一个参数是this指向的对象。null或undefined表示全局对象。第二个参数是数组,表示传入原函数的参数

    apply数组,call列表

    (1)找出数组最大元素

    js默认没有找出数组最大元素的函数,结合applyMath.max可实现返回数组的最大元素

    var a = [10, 2, 4, 15, 9];
    Math.max.apply(null, a) // 15
    

    (2)将数组的空元素变为undefined

    结合applyArray构造函数将数组的空元素变成undefined

    Array.apply(null, ['a', ,'b'])   // [ 'a', undefined, 'b' ]
    

    forEach等循环方法会跳过空元素,但是不会跳过undefined

    (3)转换类似数组的对象

    利用数组对象的slice方法,可以将一个类似数组的对象(如arguments对象)转为真正的数组。

    Array.prototype.slice.apply({0: 1, length: 1}) // [1]
    Array.prototype.slice.apply({0: 1}) // []
    Array.prototype.slice.apply({0: 1, length: 2}) // [1, 空]
    Array.prototype.slice.apply({length: 1}) // [空]
    

    (4)绑定回调函数的对象

    可以在事件方法等回调函数中,通过apply/call绑定方法调用的对象,修改this指向

    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);
    

    因为apply()/call()方法在绑定函数执行时所在的对象时,还会立即执行函数,因此需要把绑定语句写在一个函数体内。

    1. Function.prototype.bind()bind()方法将函数体内的this绑定到某个对象,然后返回一个新函数。

    如下是一个通过赋值导致函数内部this指向改变的示例。

    var d = new Date();
    d.getTime() // 1596621203097
    
    var print = d.getTime;
    print() // Uncaught TypeError: this is not a Date object.
    

    d.getTime赋值给变量print后,方法内部的this由原来指向Date对象实例改为了window对象,print()执行报错。

    使用bind()方法绑定函数执行的this指向,可以解决这个问题。

    var print = d.getTime.bind(d);
    undefined
    print()   // 1596621203097
    

    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()函数的第一个参数x5,然后返回一个新函数newAdd(),这个函数只要再接受一个参数y就能运行了。

    bind()第一个参数是nullundefined时,this绑定的是全局对象(浏览器环境为window)

    1. bind()方法特定:
    • 每一次返回一个新函数

    这就导致,如果绑定事件时直接使用bind()会绑定为一个匿名函数,导致无法取消事件绑定

    element.addEventListener('click', o.m.bind(o));
    // 如下取消是无效的
    element.removeEventListener('click', o.m.bind(o));
    

    正确写法:

    var listener = o.m.bind(o);
    element.addEventListener('click', listener);
    //  ...
    element.removeEventListener('click', listener);
    
    • 结合回调函数使用。将包含this的方法直接当做回调函数,会导致函数执行时改变了this的指向,从而出错。解决办法是使用bind()方法绑定回调函数的this对象。当然,也可使用中间变量固定this

    • 结合call()方法使用。改写一些JS原生方法的使用

    如下数组的slice方法

    [1, 2, 3].slice(0, 1) // [1]
    // 等同于
    Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
    

    call()方法实质上是调用Function.prototype.call()方法。

    // 上面等同于
    var slice = Function.prototype.call.bind(Array.prototype.slice);
    slice([1, 2, 3], 0, 1) // [1]
    

    相当于在Array.prototype.slice调用Function.prototype.call,参数为(对象,slice的参数)

    类似的写法:

    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]
    

    更进一步bind的调用也可以改写:在Function.prototype.bind上调用call方法(返回的是一个新方法),方法参数是(this对象,bind方法参数)。即最终结果是在this对象上执行bind方法并传递参数。(有些绕)

    function f() {
      console.log(this.v);
    }
    
    var o = { v: 123 };
    var bind = Function.prototype.call.bind(Function.prototype.bind);
    bind(f, o)() // 123
    

    对象的继承

    1. 对象的继承可以实现代码的复用
    2. 传统JavaScript的继承是通过"原型对象"(prototype)实现的。即js的原型链继承。ES6引入了class语法,实现基于class的继承
    3. 构造函数的缺点:构造函数中通过给this对象的属性赋值,可以很方便地定义实例对象属性。但是这种方式,同一个构造函数的多个实例之间无法共享属性。
    function Cat(name, color) {
      this.name = name;
      this.color = color;
      this.features = {
        species:'猫',
        habits:'肉食夜行动物'
      };
      this.meow = function () {
        console.log('喵喵');
      };
    }
    
    var cat1 = new Cat('大毛', '白色');
    var cat2 = new Cat('二毛', '黑色');
    
    cat1.meow === cat2.meow   // false
    cat1.features === cat2.features   // false
    

    cat1cat2是同一个构造函数的两个实例,因为所有meow方法和features对所有实例具有同样的行为和属性,应该共享而不是每个实例都创建新的方法和属性,没必要又浪费系统资源。

    原型对象(prototype)用来在实例间共享属性。

    1. JavaScript继承机制的设计思想:原型对象的所有属性和方法,都能被实例对象共享
    2. JavaScript规定,每个函数都有一个prototype属性,指向一个对象
    function f() {}
    typeof f.prototype // "object"
    

    普通函数基本不会用prototype属性

    构造函数生成实例的时候,构造函数的prototype属性会自动成为实例对象的原型。

    function Cat(name, color) {
      this.name = name;
    }
    Cat.prototype.color = 'white';
    Cat.prototype.features = {
        species:'猫',
        habits:'肉食夜行动物'
      };
    Cat.prototype.meow = function () {
        console.log('喵喵');
      };
    var cat1 = new Cat('大毛');
    var cat2 = new Cat('二毛');
    

    原型对象的属性不是实例对象自身的属性。其变动体现在所有实例对象上。

    当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。如果实例对象自身就有某个属性或方法,则不会再去原型对象寻找这个属性或方法。

    原型对象的作用,是定义所有实例对象共享的属性和方法。这也是被称为原型对象的原因。实例对象可以视作从原型对象衍生出来的子对象。

    1. JavaScript规定,所有对象都有自己的原型对象(prototype)。任何一个对象,都可以充当其他对象的原型;而由于原型对象也是对象,所以它也有自己的原型。这就形成一个"原型链"(prototype chain):对象到原型,再到原型的原型...

    2. 所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。所有对象都继承了Object.prototype的属性。

    比如所有对象都有valueOftoString方法,就是从Object.prototype继承的

    Object.prototype对象的原型是null。原型链的尽头是null

    null没有任何属性和方法,也没有自己的原型

    Object.getPrototypeOf(Object.prototype)  // null
    
    1. 如果对象自身和它的原型,都定义了一个同名属性,则优先读取对象自身的属性,这叫做"覆盖"(overriding)。

    2. prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

    function P() {}
    P.prototype.constructor === P // true
    

    constructor属性的作用是,可以得知某个实例对象由哪一个构造函数产生。另外,有了constructor属性就可以从一个实例对象新建另一个实例。

    function Constr() {}
    var x = new Constr();
    
    var y = new x.constructor();
    y instanceof Constr // true
    

    借助constructor可以在实例方法中调用自身的构造函数

    Constr.prototype.createCopy = function () {
      return new this.constructor();
    };
    
    1. constructor属性表明了原型对象与构造函数之间的关联关系。因此如果修改原型对象,一般需要同时修改constructor属性
    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.constructor === Person // true
    
    Person.prototype = {
      method: function () {}
    };
    
    Person.prototype.constructor === Person // false
    Person.prototype.constructor === Object // true
    

    修改原型对象时,一般要同时修改constructor属性的指向

    // 坏的写法
    C.prototype = {
      method1: function (...) { ... },
      // ...
    };
    
    // 好的写法
    C.prototype = {
      constructor: C,
      method1: function (...) { ... },
      // ...
    };
    
    // 更好的写法
    C.prototype.method1 = function (...) { ... };
    
    1. constructor属性的name属性返回构造函数的名称。

    2. instanceof表示对象是否为某个构造函数的实例。instanceof做判断时会检查右边构造函数的原型对象(prototype)是否在左边实例对象的原型链上。

    v instanceof Vehicle
    // 等同于
    Vehicle.prototype.isPrototypeOf(v)
    

    instanceof会检查整个原型链,因此使用instanceof判断时,实例对象的原型链上可能返回多个构造函数的原型对象

    var d = new Date();
    d instanceof Date // true
    d instanceof Object // true
    

    任意对象(除了null)都是Object的实例。

    var nullObj=null;
    typeof nullObj === 'object' && !(nullObj instanceof Object);  // true
    

    如果一个对象的原型是nullinstanceof的判断就会失真。

    利用instanceof可以解决调用构造函数时忘了加new的问题

    1. 构造函数的继承

    子类整体继承父类

    一、在子类的构造函数中调用父类的构造函数

    function Sub(value) {
      Super.call(this); // 继承父类实例的属性
      this.prop = value;
    }
    
    // 或者使用另一种写法
    function Sub() {
      this.base = Super;
      this.base();
    }
    

    二、让子类的原型指向父类的原型,继承父类原型

    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.prototype.method = '...';
    

    使用Object.create(Super.prototype)赋值给子类的原型,防止引用赋值,后面的修改影响父类的原型。

    上面是比较正确或严谨的写法。比较粗略的写法是直接将一个父类实例赋值给子类的原型

    Sub.prototype = new Super();
    

    这种方式在子类中会继承父类实例的方法(通常可能不需要具有父类的实例方法),不推荐

    子类中继承父类的单个方法

    ClassB.prototype.print = function() {
      ClassA.prototype.print.call(this);
      // self code
    }
    
    1. 多重继承:JavaScript不提供多重继承功能,即不允许一个对象同时继承多个对象。

    但是可以通过合并两个父类的原型的形式,间接变通的实现多重继承

    function M1() {
      this.hello = 'hello';
    }
    
    function M2() {
      this.world = 'world';
    }
    
    function S() {
      M1.call(this);
      M2.call(this);
    }
    
    // 继承 M1
    S.prototype = Object.create(M1.prototype);
    // 继承链上加入 M2
    Object.assign(S.prototype, M2.prototype);
    
    // 指定构造函数
    S.prototype.constructor = S;
    
    var s = new S();
    s.hello // 'hello'
    s.world // 'world'
    

    这种子类S同时继承了父类M1M2的模式又称为 Mixin(混入)

    1. JavaScript不是一种模块化编程语言,ES6才开始支持"类"和"模块"。但是可以利用对象实现模块的效果
    2. 模块是实现特定功能的一组属性和方法的封装。所以模块的实现最简单的方式就是把模块写成一个对象,所有模块成员都位于对象里面
    • 把模块写成一个对象
    var module1 = new Object({
     _count : 0,
     m1 : function (){
      //...
     },
     m2 : function (){
       //...
     }
    });
    

    函数m1m2和属性_count都封装在module1对象中。使用中直接调用这个对象的属性即可。

    但是,这种写法暴露了所有的模块成员,内部状态可以被外部改写。比如,在外部直接改写内部_count的值:module1._count = 5;

    • 使用构造函数封装私有变量

    如下,通过构造函数封装实例的私有变量

    function StringBuilder() {
      var buffer = [];
    
      this.add = function (str) {
         buffer.push(str);
      };
    
      this.toString = function () {
        return buffer.join('');
      };
    }
    

    如下,私有变量buffer在实例对象中,外部是无法直接访问的。

    但是,这种方法将私有变量封装在构造函数中,构造函数会和实例对象一直存在于内存中,无法在使用完成后清除。即构造函数的作用既用来生成实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时占用内存。

    • 构造函数中将私有变量设置为实例属性
    function StringBuilder() {
      this._buffer = [];
    }
    
    StringBuilder.prototype = {
      constructor: StringBuilder,
      add: function (str) {
        this._buffer.push(str);
      },
      toString: function () {
        return this._buffer.join('');
      }
    };
    

    这样私有变量就放在了实例对象中。但是私有变量仍然可以从外部读写

    • 通过立即执行函数封装私有变量

    通过"立即执行函数"(Immediately-Invoked Function ExpressionIIFE),通过返回"闭包"的方法和属性,实现将属性和方法封装在一个函数作用域里面,函数内的属性作为私有成员不被暴露。

    这就是js模块的基本写法:

    var module1 = (function () {
     var _count = 0;
     var m1 = function () {
       //...
     };
     var m2 = function () {
      //...
     };
     return {
      m1 : m1,
      m2 : m2
     };
    })();
    
    • 模块的放大模式
      如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时可以采用"放大模式"(augmentation)。

    如下,为模块module1添加新方法,并返回新的module1模块

    var module1 = (function (mod){
     mod.m3 = function () {
      //...
     };
     return mod;
    })(module1);
    
    • "宽放大模式"(Loose augmentation)

    在立即执行函数的参数中添加空对象,防止加载一个不存在的对象,从而报错或出意外

    var module1 = (function (mod) {
     //...
     return mod;
    })(window.module1 || {});
    
    • 全局变量的输入

    模块最重要的是"独立性"。因此为了在模块内部调用(使用)全局变量,必须显式地将其他变量输入模块内。

    比如,下面module1用到了jQuery库(模块),则可以将其作为参数输入module1。保证模块的独立性,并且表明模块之间的依赖关系

    var module1 = (function ($) {
     //...
    })(jQuery);
    

    立即执行函数还可以起到类似命名空间的作用

    Object对象的方法

    1. Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法。

    几种特殊的原型:

    // 空对象的原型是 Object.prototype
    Object.getPrototypeOf({}) === Object.prototype // true
    
    // Object.prototype 的原型是 null
    Object.getPrototypeOf(Object.prototype) === null // true
    
    // 函数的原型是 Function.prototype
    function f() {}
    Object.getPrototypeOf(f) === Function.prototype // true
    
    1. Object.setPrototypeOf方法为参数对象设置原型,返回该参数对象。Object.setPrototypeOf(obj,prototypeObj)

    new命令可以使用Object.setPrototypeOf方法模拟。

    var F = function () {
      this.foo = 'bar';
    };
    var f = new F();
    
    // 等同于
    var f = Object.setPrototypeOf({}, F.prototype);
    F.call(f);
    
    1. Object.create方法以一个对象为原型,返回一个实例对象。该实例完全继承原型对象的属性。
    // 原型对象
    var A = {
      print: function () {
        console.log('hello');
      }
    };
    
    // 实例对象
    var B = Object.create(A);
    
    Object.getPrototypeOf(B) === A // true
    B.print() // hello
    B.print === A.print // true
    

    Object.create方法的实现可以用下面的代码代替

    if (typeof Object.create !== 'function') {
      Object.create = function (obj) {
        function F() {}
        F.prototype = obj;
        return new F();
      };
    }
    

    生成新的空对象,如下四种是等价的

    var obj1 = Object.create({});
    var obj2 = Object.create(Object.prototype);
    var obj3 = new Object();
    var obj4 = {};
    

    Object.create的参数为null可以生成一个不继承任何属性(没有toStringvalueOf方法)的对象

    var obj = Object.create(null);
    

    Object.create方法必须指定参数且为对象,否则报错。Object.create创建的对象的原型是引用赋值,即动态继承原型。

    Object.create方法还可以接受的第二个参数是属性描述对象,描述的对象属性会添加到实例对象的自身属性上。

    var obj = Object.create({}, {
      p1: {
        value: 123,
        enumerable: true,
        configurable: true,
        writable: true,
      },
      p2: {
        value: 'abc',
        enumerable: true,
        configurable: true,
        writable: true,
      }
    });
    
    // 等同于
    var obj = Object.create({});
    obj.p1 = 123;
    obj.p2 = 'abc';
    

    Object.create方法生成的对象会继承它的原型对象的构造函数。

    1. Object.prototype.isPrototypeOf():实例对象的isPrototypeOf方法判断该对象是否为参数对象原型链上的原型。

    Object.prototype位于除了直接继承自null的对象之外的所有对象的原型链上。

    Object.prototype.isPrototypeOf({}) // true
    Object.prototype.isPrototypeOf([]) // true
    Object.prototype.isPrototypeOf(/xyz/) // true
    Object.prototype.isPrototypeOf(Object.create(null)) // false
    
    1. 关于__proto__属性。__proto__属性是实例对象的属性,表示实例对象的原型(可读写)。实例对象(或非函数对象)无法通过prototype属性获取原型(只有参数才有prototype属性),而__proto__属性默认应该是私有属性,不应该被读写,并且__proto__属性只有浏览器才需要部署。因此,对原型的读写操作正确做法是使用Object.getPrototypeOf()Object.setPrototypeOf()

    Obj可以用__proto__直接设置原型

    1. 关于__proto__prototype属性

    如下,为构造函数、实例对象、普通对象中__proto__和prototype的对比

    /** 构造函数的__proto__和prototype **/
    var P=function(){}
    
    P.prototype
    // {constructor: ƒ}
    
    P.__proto__
    // ƒ () { [native code] }
    
    P.__proto__===P.prototype
    // false
    
    P.__proto__===P.constructor.prototype
    // true
    
    P.__proto__===Object.getPrototypeOf(P)
    // true
    
    P.__proto__===Function.prototype
    // true
    
    P.constructor===Function
    // true
    
    /** 实例对象的__proto__和prototype  **/ 
    var p=new P()
    
    p.prototype
    // undefined
    p.__proto__
    // {constructor: ƒ}
    p.__proto__===Object.getPrototypeOf(p)
    // true
    
    p.__proto__===P
    // false
    p.__proto__===P.prototype
    // true
    
    p.constructor===P
    // true
    
    /** 实例对象的__proto__和prototype **/
    var obj={}
    
    obj.prototype
    // undefined
    
    obj.__proto__===Object.getPrototypeOf(obj)
    // true
    
    obj.__proto__===Object.prototype
    // true
    
    obj.constructor===Object
    // true
    
    var nullObj=Object.create(null)
    
    nullObj.__proto__
    // undefined
    nullObj
    // {}无属性
    

    几点总结:

    • js中,对象的原型通过__proto__属性获取,由此组成原型链及原型链的继承。

    • __proto__是对象自带的属性,除了null和原型对象为null的对象之外,所有的对象都有__proto__属性。函数是对象,因此函数也有__proto__属性

    • prototype属性是函数独有的属性,每个函数都有一个prototype属性对象,作用是在实例对象间共享属性和方法。因此prototype只会在构造函数中使用,表示实例对象的原型对象。面向对象中的继承由此实现。

    • __proto__属性指向当前对象的原型对象,即构造函数的prototype属性。

    • constructor属性表示当前对象的构造函数

    • 函数也是对象,因此也拥有__proto__属性,指向当前函数的构造函数的prototype属性。一个函数的constructorFunction__proto__Function.prototype

    1. __proto__属性指向当前对象的原型对象,即构造函数的prototype属性。
    var obj = new Object();
    
    obj.__proto__ === Object.prototype
    // true
    obj.__proto__ === obj.constructor.prototype
    // true
    
    1. 获取一个对象obj的原型对象,有三种办法:
    • obj.__proto__
    • obj.constructor.prototype
    • Object.getPrototypeOf(obj)

    但是 __proto__属性只有浏览器环境才需要部署。obj.constructor.prototype在手动改变原型对象时,可能会失效

    如下,将构造函数C的原型对象改为p后。实例对象c.constructor.prototype却没有指向pObject.getPrototypeOf(obj)正确获取原型对象,是获取原型对象推荐使用的方法

    var P = function () {};
    var p = new P();
    
    var C = function () {};
    C.prototype = p;
    var c = new C();
    
    c.constructor.prototype === p // false
    
    c.constructor.prototype === P.prototype   // true
    
    Object.getPrototypeOf(c) === p  // true
    

    上面变更原型对象的方法是不正确的。通常修改prototype时,要同时设置constructor属性。

    C.prototype = p;
    C.prototype.constructor = C;
    
    var c = new C();
    c.constructor.prototype === p // true
    
    1. Object.getOwnPropertyNames()返回对象自身所有属性的键名组成的数组(包括可遍历和不可遍历的所有属性)。

    2. Object.keys返回对象自身所有可遍历的属性名组成的数组

    3. Object.prototype.hasOwnProperty()返回一个属性是否为对象自身的属性

    hasOwnProperty方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法

    1. in运算符表示一个对象是否具有某个属性。即检查一个属性是否存在。
    'length' in Date // true
    'toString' in Date // true
    

    for...in循环可以获取一个对象所有可遍历的属性(自身和继承的属性)

    通常使用如下方式,遍历对象自身的属性

    for ( var name in object ) {
      if ( object.hasOwnProperty(name) ) {
        /* loop code */
      }
    }
    
    1. 获取一个对象的所有属性(包含自身的和继承的,以及可枚举和不可枚举的所有属性)
    function inheritedPropertyNames(obj) {
      var props = {};
      while(obj) {
        Object.getOwnPropertyNames(obj).forEach(function(p) {
          props[p] = true;
        });
        obj = Object.getPrototypeOf(obj);
      }
      return Object.getOwnPropertyNames(props);
    }
    
    1. 对象的拷贝

    要拷贝一个对象,需要做到下面两点:

    • 确保拷贝后的对象,与原对象具有同样的原型。
    • 确保拷贝后的对象,与原对象具有同样的实例属性。

    如下,为对象拷贝的实现:

    function copyObject(orig) {
      var copy = Object.create(Object.getPrototypeOf(orig));
      copyOwnPropertiesFrom(copy, orig);
      return copy;
    }
    
    function copyOwnPropertiesFrom(target, source) {
      Object
        .getOwnPropertyNames(source)
        .forEach(function (propKey) {
          var desc = Object.getOwnPropertyDescriptor(source, propKey);
          Object.defineProperty(target, propKey, desc);
        });
      return target;
    }
    

    利用ES2017引入的Object.getOwnPropertyDescriptors可以更简便的实现

    function copyObject(orig) {
      return Object.create(
        Object.getPrototypeOf(orig),
        Object.getOwnPropertyDescriptors(orig)
      );
    }
    

    严格模式(strict mode)

    1. JavaScript提供代码执行的第二种模式:严格模式。严格模式从ES5引入,主要目的为:
    • 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
    • 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
    • 提高编译器效率,增加运行速度。
    • 为未来新版本的 JavaScript 语法做好铺垫。
    1. 严格模式的启用:在代码头部添加一行'use strict';即可。老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式。
    2. use strict放在脚本文件的第一行,整个脚本都将以严格模式运行。不在第一行则无效。
    3. use strict放在函数体的第一行,则整个函数以严格模式运行。
    4. 有时需要把不同脚本文件合并到一个文件。这时,如果一个是严格模式另一个不是,则合并后结果将会是不正确的。解决办法是可以把整个脚本文件放在一个立即执行的匿名函数中:
    (function () {
      'use strict';
      // some code here
    })();
    
    1. 严格模式下的显式报错

    严格模式下js的语法更加严格,许多在正常模式下不会报错的错误代码都会显式的报错

    如下几项操作严格模式下都会报错:

    • 只读属性不可写;比如字符串的length属性

    • 不可配置属性无法删除(non-configurable)

    • 只设置了取值器的属性不可写

    • 禁止扩展的对象不可扩展

    • evalarguments 不可用作标识名

    正常模式下,如果函数有多个重名的参数,可以用arguments[i]读取。严格模式下属于语法错误。

    • 函数不能有重名的参数

    • 禁止八进制的前缀0表示。八进制使用数字0和字母O表示

    1. 严格模式下的安全限制
    • 全局变量显式声明
    • 禁止this关键字指向全局对象。避免无意中创造全局变量
    // 正常模式
    function f() {
      console.log(this === window);
    }
    f() // true
    
    // 严格模式
    function f() {
      'use strict';
      console.log(this === undefined);
    }
    f() // true
    

    严格模式下,函数直接调用时,内部的this表示undefined(未定义),因此可以用callapplybind方法,将任意值绑定在this上面。正常模式下,this指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而nullundefined这两个无法转成对象的值,将被忽略。

    • 函数内部禁止使用 fn.calleefn.caller

    • 禁止使用arguments.calleearguments.caller

    arguments.calleearguments.caller是两个历史遗留的变量,从来没有标准化过,现在已经取消

    • 禁止删除变量。严格模式下使用delete命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的configurable属性设置为true,才能被delete命令删除。
    1. 静态绑定
    • 禁止使用with语句

    • 创设eval作用域

    正常模式下,JavaScript语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:eval作用域。

    eval所生成的变量只能用于eval内部。

    (function () {
      'use strict';
      var x = 2;
      console.log(eval('var x = 5; x')) // 5
      console.log(x) // 2
    })()
    

    eval语句使用严格模式:

    // 方式一
    function f1(str){
      'use strict';
      return eval(str);
    }
    f1('undeclared_variable = 1'); // 报错
    
    // 方式二
    function f2(str){
      return eval(str);
    }
    f2('"use strict";undeclared_variable = 1')  // 报错
    
    • arguments不再追踪参数的变化。严格模式下参数修改,arguments不再联动跟着改变
    1. 面向ECMAScript 6
    • ES5的严格模式只允许在全局作用域或函数作用域声明函数。
    • 保留字。严格模式新增了一些保留字:implementsinterfaceletpackageprivateprotectedpublicstaticyield
    作者: 代码迷途
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意原创文章必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
    非原创文章若有需要,建议直接联系原文作者或保留声明情况下转载原文
  • 相关阅读:
    时间日期事件处理、长按事件
    单选按钮触发事件、下拉列表触发事件
    事件
    笔记3
    笔记2
    笔记1
    布局管理器
    08、shell三剑客之sed
    07、shell三剑客之grep
    06、shell正则表达式
  • 原文地址:https://www.cnblogs.com/codemissing/p/JavaScript_Object_Oriented_OOP.html
Copyright © 2011-2022 走看看