JS的面向对象浅析

与其它OOP语言 (C++ Java)不同,JS 中没有class 的概念, JS中的对象是以构造函数和原型链作为模板的.

构造函数类似于普通的函数,但是有其特点,一般约定函数名首字母大写,函数体中有this, 带表生成的对象实例.

生成对象时,必须使用 new 命令.

New 命令

  1. 基本用法

    new 命令的作用就是执行构造函数,返回一个实例对象.

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

    如果忘了使用 new 命令,直接调用构造函数会发生什么事?

    这时候,构造函数就变成了普通函数,不会生成实例对象,而且this 会代表全局对象,将造成一些意想不到的结果.

    为了保证构造函数必须与 new 命令一起使用,可以在构造函数内容使用严格模式,如此一来,一旦忘记使用 new 命令,就会报错.

    function Cons(){
      'use strict';
      this.a = 1;
    }
    Cons();
    // TypeError:Cannot set property 'a' of undefined
    

    这是因为严格模式下,this不能指向全局变量,默认为undefined 导致报错.

    反之,如果对普通函数使用 new 命令,则会返回一个空对象,不管这个普通函数内部返回的是什么.

  2. new 命令的原理

    使用 new 命令时,会发生以下事情:

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

    也就是说,this指向的是一个新生成的空对象,任何对this的操作都会发生在这个空对象上, 如果构造函数内部有 return ,并且 return 后面跟着一个对象,那么 new 命令就返回 return 语句指定的对象,否则就忽略 return 语句,返回 this 对象.

  3. **Object.create( ) 创建实例对象 **

    利用现有的对象作为模板,生成新的实例对象.

    var person1 = {
      name:'wjk',
      age:'33',
      greeting:function(){
        console.log('Hi! I\'m'+this.name + '.');
      }
    }
    
    var person2 = Object.create(person1);
    person2.name //wjk
    person2.greeting() // Hi! I'm wjk
    

this 关键字

this 关键字是个非常重要的语法点,不理解它的含义,大部分开发任务都无法完成.

this 有一个共同点:它总是返回一个对象. this 就是属性或者方法”当前”所在的对象.

对象的属性可以赋值给另和个对象,所以属性所在的当前对象是可变的,也就是说 this 的指向是可变的.

function f() {
  return '姓名:'+ this.name;
}

var A = {
  name: '张三',
  describe: f
};

var B = {
  name: '李四',
  describe: f
};

A.describe() // "姓名:张三"
B.describe() // "姓名:李四"

JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,this的指向是动态的,没有办法事先确定到底指向哪个对象,这才是感到困惑的地方。

this 的实质

对象名和函数名其实是对象和函数的内存地址,所以拿到函数的引用后,就可以在不同的环境中执行,而JS允许在函数体中引用当前环境的其它变量,所以要有一种机制,能够在函数体中获得当前的运行环境,this就出现了,它的设计目的就是在函数体内部指代函数当前的运行环境.

使用 this 的注意点

​ 由于 this 的指向是不确定的,所以不要在函数中包含多层的 this.

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

o.f1()
// Object
// Window

绑定 this 的方法

this的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this固定下来,避免出现意想不到的情况。JavaScript 提供了**callapplybind**这三个方法,来切换/固定this的指向。

  • Function.prototype.call( )

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

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

    call 方法还可以接受多个参数

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

    后面的 arg 们 是函数调用时所需的参数.

  • Function.prototype.apply

    用法与 call 方法一样,不同之处是它接收一个数组作为函数执行时的参数.

    找出数组中最大的数

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

    把类数组对象转换为数组

    Array.prototype.slice.apply({0:1,length:1})
    // [1]
    Array.prototype.slice.apply({0:1})
    //[]
    
    
    
  • Fuction.prototype.bind( )

    用于将某个对象绑定到函数体内的 this ,然后返回一个新函数

    var counter = {
      count: 0,
      inc: function () {
        this.count++;
      }
    };
    
    var func = counter.inc.bind(counter);
    func();
    counter.count // 1
    
    

    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的第一个参数绑定成了5,然后返回一个新函数 newAdd, 此函数只要再接受一个参数y就行了.

    注意:bind 方法每次运行都会返回一个新函数

    所以这在事件绑定写法中要注意,否则绑定事件后,无法接触绑定

    正确的方法是写成下面这样:

    var listener = o.m.bind(o);
    element.addEventListener('click', listener);
    //  ...
    element.removeEventListener('click', listener);
    
    

    利用bind方法可以改写一些原生 JS 方法的使用形式

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

    上面的call 方法实际上是调用的Function.prototype.call

    var newSlice = Function.prototype.call.bind(Array.prototype.slice);

    所以

    newSlice([1,2,3],0,1)

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

    上面的含义是将Array.prototype.slice 变成 Function.prototype.call 方法所在的对象,调用时,就变成了slice在call

    如果再进一步,把bind方法变成call所在的对象,就意味着返回的新bind方法就会变成bind在call

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

    上面代码 newBind(f,o) 返回一个新函数,这个函数绑定了执行对象o,所以会打印出log 123.

对象的继承

​ 大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现

  1. 原型对象

    • 1.1构造函数的缺点

      同一个构造函数的多个实例之间,无法共享属性:

      function Cat(name, color) {
        this.name = name;
        this.color = color;
        this.meow = function () {
          console.log('喵喵');
        };
      }
      
      var cat1 = new Cat('大毛', '白色');
      var cat2 = new Cat('二毛', '黑色');
      
      cat1.meow === cat2.meow
      // false
      
      

      每新建一个实例,就会新建一个 meow 方法,这既没有必要又浪费了系统资源,因为方法都是同样的行为,完全应该共享.

      这个问题的解决方法就是JS的原型对象 prototype

    • 1.2 prototype 属性的作用

      如果属性和方法定义在原型上,那么所有的实例对象都能共享,不仅节省了内存,还体现了实例对象之间的联系.

      JS 规定 每个函数都具有prototype属性;

      普通函数的prototype属性没有多大意义;

      但是对于构造函数的prototype来说, 通过构造函数生成的实例对象,它们的prototype都会指向构造函数的prototype属性;

      prototype属性的值是一个对象,也叫做原型对象,它负责定义所有实例对象共享的属性和方法,这也是它被叫做原型对象的原因,而实例对象可以看作是从原型对象衍生出来的子对象.

    • 1.3 原型链

      所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……

      所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOftoString方法的原因,因为这是从Object.prototype继承的。

      那么,Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null

    • 1.4 constructor 属性

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

      由于 constructor 属性定义在 prototype 上面,意味着可以被所有实例对象继承.

      constructor 属性的作用是,可以得知某个对象到底是哪一个构造函数产生的.

      function F(){};
      var f = new F();
      f.constructor === F //true
      f.constructor === F.prototype.constructor //true
      
      
  2. instanceof 运算符

    instanceof 运算符的左边是实例对象,右边是构造函数

    返回一个布尔值,表示对象是否为某个构造函数的实例.

    它会检查右边构造函数的原型对象(prototype),看是否在左边对象的原型链上。

    由于 instanceof 检查实例对象的整个原型链,因此同一个实例对象,可能会对多个构造函数都返回 true

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

    任意对象 除了 null 都是Object 的实例,所以 instanceof 运算符可以判断一个值是不是 null 的对象

    var target;

    target instanceof Object

    有一种情况除外,就是左边实例对象的原型是null 这样,instanceof 的判断就会失真,

    Object.create(null) instanceof Object

    在这句代码中,会检查Object 的prototype属性是否在Object.create(null) 实例的原型链上,而这个实例的原型是null 所以不在它的原型链上,结果返回 false,但实际上 Object.create(null) 就是一个Object实例.

    注意: instanceof 运算符是能用于对象 ,不能用于原始类型的值

    利用instanceof运算符,还可以巧妙地解决,调用构造函数时忘了加new命令的问题。

    function Fubar (foo, bar) {
      if (this instanceof Fubar) {
        this._foo = foo;
        this._bar = bar;
      } else {
        return new Fubar(foo, bar);
      }
    }
    
    
  3. 构造函数的继承

    function Shape(){
      this.x = 0;
      this.y =0;
    }
    Shape.prototype.move = function(x, y){
      this.x += x;
      this.y += y
    }
    
    //新建 Rectangle 继承 Shape
    
    function Rectangle(){
      Shape.call(this);
    }
    Rectangle.prototype = Object.create(Shape.prototype);
    Rectangle.prototype.constructor = Rectangle;
    
    
    
  4. 多重继承

    JS 不提供多重继承,可以通过变通方法来实现

    function M1(){
      this.hello = 'hello';
    }
    function M2(){
      this.world = 'world';
    }
    
    function S(){
      M1.call(this);
      M2.call(this);
    }
    
    S.prototype =Object.create(M1.prototype);
    Object.assign(S.prototype,M2.prototype);
    S.prototype.constructor = S;
    
    
    
  5. 模块

如何利用对象实现模块化的效果

  • 5.1 基本的实现方法

    模块是实现特定功能的一组属性和方法的封装.

    简单的做法是把模块写成一个对象

    但是这样做会暴露内部所有成员,内部属性也可以随时被外部改写.

  • 5.2 封装私有变量:构造函数的写法

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

    上面代码中,buffer是模块的私有变量。一旦生成实例对象,外部是无法直接访问buffer的。但是,这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时,非常耗费内存。

  • 5.3 封装私有变量:立即执行函数的写法

    将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的.

    var module1 = (function(){
      var _count = 0;
      var m1 = function(){};
      var m2 = function(){};
      return {
        m1:m1,
        m2:m2
      }
    })();
    
    
  • 5.4 模块的放大模式

    (function($, window, document) {
    
      function go(num) {
      }
    
      function handleEvents() {
      }
    
      function initialize() {
      }
    
      function dieCarouselDie() {
      }
    
      //attach to the global scope
      window.finalCarousel = {
        init : initialize,
        destroy : dieCarouselDie
      }
    
    })( jQuery, window, document );
    
    

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
    
    
  2. Object.setPrototypeOf( )

    为参数对象设置原型,接受两个参数,一个是现有对象,二个是原型对象

    Object.setPrototypeOf 方法实现对 new 命令的模拟

    var F = function (){
      this.foo = 'bar';
    }
    
    var f = Object.setPrototypeOf({},F.prototype);
    // then bind f to this
    F.call(f);
    
    
  3. Object.create( )

    生成实例一般通过 new 加构造方法实现,但是有时候,只有一个现成的实例,或者找不到相应的构造方法怎么办?

    Object.create( ) 用来接受一个对象实例,然后以这个对象实例为原型,返回一个对象,该对象完全继承原型对象的属性.

    此方法的模拟:

    if(typeof Object.create !=='function'){
      //如果当前环境没有Object.create方法
      Object.create = function(obj){
        var F = function(){};
        F.prototype = obj;
        return new F();
    }
    
    

    除了原型对象,该方法还接受第二个参数:属性描述对象; 它所描述的对象属性会添加到返回的实例对象,作为该实例对象自身的属性.

    var obj = Object.create({},{
      p:{
        value:123,
        enumerable:true,
        configurable:true,
        writable:true
      },
      p2:{
        value:"abc",
        enumerable:true,
        configurable:true,
        writable:true
      }
    });
    // 等同于
    var obj = Object.create({});
    obj.p = 123;
    obj.p2 = "abc";
    
    
    
  4. Object.prototype.isPrototypeOf( )

    实例对象的 isPrototypeOf 方法用来判断该对象是否是参数对象的原型, 方法由原型对象来调用.

    只要是处在参数对象原型链上的原型,调用此方法时,都会返回 true

  5. Object.prototype.__proto__

    实例对象的 __proto__ 属性,返回该对象的原型. 该属性可读写.

    根据语言标准,__proto__ 属性只有浏览器才需要部署,其它环境可以没有这个属性.它的前后分别有两个下划线,表明是一个内部属性,不应该对使用者暴露,因此,应该尽量少用这个属性,而总是用 Object.getPrototypeOf() and Object.setPrototypeOf() 进行原型对象的读写.

  6. Object.getOwnPropertyNames( )

    返回所有自身属性名组成的数组,包括不可枚举属性,

    如果要获取可枚举属性,用Object.keys( );

  7. Object.prototype.hasOwnProperty( )

    返回参数是在对象自身定义,还是定义在原型链上.

    它是JS 中 唯一一个处理对象属性时,不会遍历原型链的方法.

  8. in 运算符和 for … in 循环

    in 运算符返回一个布尔值, 表示一个对象是否具有某个属性,它不区分该属性是自身属性还是继承来的属性.

    获取对象所有可遍历属性,不管是自身的还是继承的,可以使用 for in 循环.

    var o1 = {p1:123};
    var o2 = Object.create(o1,{
      p2:{value:"ab",enumerable:true}
    });
    for(t in o2){
      console.log(t);
    }
    //p2
    //p1
    
    

    下面的方法用于获取对象的所有属性,包括继承来的和不可枚举的

    function getAllpropertyNames(obj){
      var props = {};
      while(obj){
        Object.getOwnPropertyNames(obj).forEach(function(p){
     		props[p] = true;     
        });
        obj = Object.getPrototypeOf(obj);
      }
      return Object.getOwnPropertyNames(props)
    }
    
    
  9. 对象的拷贝

    • 确保拷贝后的对象与原对象有同样的原型
    • 确保拷贝后的对象与原对象有同样的属性
    function objCopy(origin){
      var obj = Object.create(Object.getPrototypeOf(origin));
      copyOwnPropertiesFrom(copy,orig);
      return copy;
    }
    
    function copyOwnPropertiesFrom(target,from){
      Object.getOwnPropertyNames(from).forEach(function(p){
        var des = Object.getOwnPropertyDescriptor(from,p);
        Object.defineProperty(target,p,des);
      });
      return target;
    }
    
    
    

    另一种更简便的写法是 ES2017引入的

    Object.getOwnProperyDescriptors

    function copyObject(orig){
      return Object.create(Object.getPrototypeOf(orig),
                 Object.getOwnPropertyDescriptors(orig)         
                          );
    }
    
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值