JavaScript继承、属性检测及遍历

1. 原型回顾

上次讲原型和原型链,对比Java中的继承和重写其实不难理解,原型是function对象的一个属性,它定义了构造函数构造出的对象的公共祖先,由构造函数产生的对象,可以继承对应原型的属性和方法,原型也是对象。

原型链就是把原型串起来,在原型上面再加一个原型,再加一个原型,使用__proto__连接各个原型。

还学到了原型链的增删改查,一般情况下不能通过后代改父代,但要注意可以修改的情况。JavaScript原型及原型链

还有call/apply/bind的作用和不同点,作用是改变this指向,在企业开发中可用于组装部件。

2. 继承模式

原型继承有好几种方式,这里只讲了其中有实在意义的几种,其中寄生组合式继承是最常用也是最成熟的方法。

对于没讲的继承方法可以参考:JavaScript学习笔记(十四) 继承

extend走过许多发展史

2.1. 原型继承

原型链只是继承的一种方法,虽然简单,但是它也会继承许多没用的属性

2.2. 构造函数继承

于是有了call/apply那种借用构造函数的方式,虽然它不太像继承。缺点是它不能继承借用构造函数的原型,且每次构造函数都要走一个函数,从视觉上减少了代码量,但是运行成本并没有减少。

2.3. 共享原型模式

然后到了现在使用很多的共享原型的模式,即多个构造函数使用一个原型,直接赋值即可,比如Target.prototype = Origin.prototype;

共享原型的模式我们会发现Target.prototypeOrigin.prototype完全指向了一个空间,修改其中一个另一个势必也会跟着修改,这或许不是我们想要的,寄生组合式继承(圣杯)模式恰好解决了这种问题。

2.4. 组合继承

这种继承方式是我以前不知道的,它结合了原型继承和构造函数继承,综合二者优势。

通过原型继承实现原型属性和原型方法的继承,通过构造继承实现实例属性和实例方法的继承

组合继承:参考 wsmrzx: https://blog.csdn.net/wsmrzx/article/details/104547040

      function SuperType(name, info) {
         // 实例属性(基本类型)
         this.name = name || 'Super';
         // 实例属性(引用类型)
         this.info = info || ['Super'];
         // 实例方法
         this.getName = function () {
            return this.name;
         }
      }
      // 原型方法
      SuperType.prototype.getInfo = function () {
         return this.info;
      }

      // 组合继承
      function ChildType(name, info, message) {
         SuperType.call(this, name, info);
         this.message = message;
      }
      ChildType.prototype = new SuperType();
      ChildType.prototype.constructor = ChildType;

      // 在调用子类构造函数时,可以向父类构造函数传递参数
      var child = new ChildType('Child', ['Child'], 'Hello');

      // 子类实例可以访问父类的实例方法和原型方法
      console.log(child.getName()); // Child
      console.log(child.getInfo()); // ["Child"]

      // 每个子类实例的属性独立存在
      var other = new ChildType('Child', ['Child'], 'Hi');
      other.info.push('Temp');
      console.log(other.info); // ["Child", "Temp"]
      console.log(child.info); // ["Child"]

2.5. 寄生组合式继承(圣杯模式)

ES6 中新增的 extends 底层也是基于寄生式组合继承的

      Father.prototype.name = "Tian";
      function Father() {}
      function Son() {}

      function inherit(Target, Origin) { //传入Son,Father
         function F() {};
         F.prototype = Origin.prototype;
         Target.prototype = new F();
         Target.prototype.constucor = Target; //纠正构造函数为Son,而不应是Father
         Target.prototype.uber = Origin; //超级父类,表示到底继承自谁?
      }
      inherit(Son, Father);
      var son = new Son();

在雅虎YUI库中(参考:http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js)

有关继承的封装好的函数,是像下面这样写的,有什么好处呢?

      var inherit = (function () {
         var F = function () {};
         return function (Target, Origin) {
            F.prototype = Origin.prototype;
            Target.prototype = new F();
            Target.prototype.constuctor = Target;
            Target.prototype.uber = Origin.prototype;
         }
      }());

好处是使用了闭包和立即执行函数,私有化变量,不污染执行环境,对比前一种方法,F根本只起了一个中间层的作用

讲到闭包产生的私有化变量,实际上类似于C++中的private类型的变量,C++中可以通过程序给出的一些接口来访问它或者操作它,但是你想直接访问或者操作它就不行,同理使用闭包可以达到类似效果,下面代码中money就是一个私有化变量,在外部无法直接访问,但是可以通过给出的函数来操作它

      function Person(name, age) {
         this.name = name;
         var money = 100;
         this.earnMoney = function () {
            money++;
            console.log(money);
         }
         this.payMoney = function () {
            money--;
            console.log(money);
         }
      }
      var per = new Person("Tian", 22);
      per.earnMoney();
      per.payMoney();

3. 命名空间

namespace一般就是对象,大型项目有多人开发时,可能存在定义了同名变量,那么就把变量放到命名空间统一管理,防止污染全局,适用于模块化开发,同时此处的模块化开发也是闭包的用处之一

现在流行的解决方式是webpack

4. 对象枚举

首先讲访问对象属性的方式

      // 键可以是任意字符串,值可以是任意数据类型
      var person = {
         name: {
            // 键值之间用冒号“:”分隔
            'first-name': 'Steve',
            'last-name': 'Jobs'
            // 此处键是一个字符串,可以使用方括号运算符访问
            // 如person["age"]  输出48
            // person["name"]["first-name"]  输出"Steve"
         },
         isMale: true,
         age: 48
      }

4.1. 方括号运算符

方括号运算符内是一个字符串,也可以是表达式,但表达式必须返回字符串或一个可以转换为字符串的值(因为有些代码就是使用字符串来作为对象的键)
这个方括号运算符是比较灵活的,实际上点运算符在内部式转换为方括号来运行的,而且方括号具有拼接字符串的功能,比如此处将数字与wife拼接就不需要使用繁琐的判断循环了。

      var Tian = {
         wife1: "Yang",
         wife2: "Wu",
         wife3: "He",
         wife4: "Luo",
         wife5: "Tong",
         wife6: "Wang",
         SayWife: function (num) {
            return this['wife' + num];
         }
      }

4.2. 点运算符

右侧必须是一个标识符,只能表示键是一个合法的标识符且不是一个保留字的情况
若在属性中没有找到相关的键,就会返回undefined,这样可能会报错,所以在前后端交换数据时,为了避免直接报错,可以使用或(||)运算符或者与(&&)运算符

其中属性分为自有属性(直接在对象中定义的属性) 和继承属性(在当前对象的原型对象中定义的属性)

什么是属性的可枚举性?
答:属性的特性之一,用来描述属性是否可以用一般的遍历操作获取到值,可枚举属性是其内部可枚举标志设置为true的那些属性,这是通过简单赋值或通过属性初始化器创建的属性的默认值(通过Object.defineProperty定义的属性,此类默认可枚举为false)。

实际上对于一组数据的遍历过程,也相当于枚举(Enumerable)

回到枚举,当你想知道对象中有多少个属性时,就需要用到枚举,比如for...in循环

      var Tian = {
         wife1: "Yang",
         wife2: "Wu",
         wife3: "He",
         wife4: "Luo",
         wife5: "Tong",
         wife6: "Wang",
         SayWife: function (num) {
            return this['wife' + num];
         }
      }
      for (var sx in Tian) {
         console.log(Tian.sx); //输出7个undefined,为什么?
      }
      for (var sx in Tian) {
         console.log(Tian[sx]); //输出七个键值
      }
      for (var sx in Tian) {
         console.log(sx); //输出七个键名
      }

输出7个undefined是为什么?这是因为Tian.sx在内部会被转换为Tian['sx'],这个时候会去访问Tian内部有没有sx这个属性,很明显没有。正确的应该这么写,在写对象的属性是尽量使用方括号运算符

但是此处for...in循环会把所有的可枚举属性都拿出来,包括原型的属性,但有时候这并不是我们想要的。此时可以通过hasOwnProperty()进行判断,还需注意for...in循环不会打印顶端系统自带的属性,也就是Object.prototype上的属性

附上developer.mozilla属性访问器

5. 属性检测

参考:JavaScript 中对象属性存在性及相关检测方法总结

JS有三种方法检测某个属性是否在特定的对象中

5.1. in

  • 会检查原型链
  • 不论是否可枚举

检测属性(键名)是否为特定对象的自有属性继承属性 例如'name' in person输出true,也就是说它相当于只能判断该属性能不能在对象上访问到(不论属性是否可枚举都会检查)

      function Person(name) {
         this.name = name;
      }
      Person.prototype.sex = "male";

      function Student(age) {
         this.age = age;
      }
      Student.prototype = new Person("Tian");
      var per = new Student(12);
      console.log("age" in per);//true
      console.log("sex" in per);//true
      console.log("name" in per);//true

5.2. hasOwnProperty()

  • 不检查原型链
  • 不论是否可枚举

顾名思义:确定JS对象是否具有指定的自有属性。强调自身的属性。
该操作仅会检查属性在对象本身是否存在,不会检查其[[prototype]]原型链。

      function Person(name) {
         this.name = name;
      }
      Person.prototype.sex = "famale";

      function Student(age) { //自己的
         this.age = age;
      }
      Student.prototype = new Person("Tian");
      Student.prototype.hobby = "basketball";
      var per = new Student(12);
      // Object.defineProperty(per, "sex", {
      //    enumerable: false,
      //    value: "male"
      // });
      Object.defineProperty(per, "hobby", {
         enumerable: false,
         value: "code"
      });
      // name和sex是父级的
      // age和hobby是自己的
      // sex和hobby不可枚举
      console.log(per.hasOwnProperty('name')); //false
      console.log(per.hasOwnProperty('sex')); //false
      console.log(per.hasOwnProperty('age')); //true
      console.log(per.hasOwnProperty('hobby')); //true

附加:上面代码中有几行被我注释掉了,如下,如果你运行了这几行代码,那么sex属性就会变成Student的,此时父级的__proto__仍然有sex

      Object.defineProperty(per, "sex", {
         enumerable: false,
         value: "male"
      });

一般来讲,所有普通对象都可以通过对Object.prototype的委托来访问hasOwnProperty(),但是,对于有些特殊情况,比如通过Object.create(null)来创建的对象,它就没有连接到Object.prototype,因此就无法使用该方法:

5.3. propertyIsEnumerable()

  • 不检查原型链
  • 要求可枚举

该方法返回一个布尔值,该布尔值指示指定的属性 是否可枚举 并且 是对象自己的属性。例如person.propertyIsEnumerable("name")输出true

      function Person(name) {
         this.name = name;
      }
      Person.prototype.sex = "famale";

      function Student(age) { //自己的
         this.age = age;
      }
      Student.prototype = new Person("Tian");
      Student.prototype.hobby = "basketball";
      var per = new Student(12);
      Object.defineProperty(per, "sex", {
         enumerable: false,
         value: "male"
      });
      Object.defineProperty(per, "hobby", {
         enumerable: false,
         value: "code"
      });
      // name和sex是父级的
      // age和hobby是自己的
      // sex和hobby不可枚举
      console.log(per.propertyIsEnumerable('name')); //false
      console.log(per.propertyIsEnumerable('sex')); //false
      console.log(per.propertyIsEnumerable('age')); //true
      console.log(per.propertyIsEnumerable('hobby')); //false

6. 属性遍历

JS提供三种方法遍历对象中的属性

6.1. for...in

  • 会检查原型链
  • 要求可枚举

该操作会检查属性在对象本身及其[[prototype]]原型链中是否存在。

遍历由字符串键控的对象的所有可枚举属性(忽略由Symbol键控的那些),包括继承的可枚举属性

      var person = {
         name: {
            'first-name': 'Steve',
            'last-name': 'Jobs'
         },
         isMale: true,
         age: 48
      }
      for (var sx in person) {
         console.log(sx);
      }
      Object.defineProperty(person, 'isMale', {
         enumerable: false
      })
      console.log("将person的isMale属性设为不可枚举之后")
      for (var sx in person) {
         console.log(sx);
      }
      // 输出
      // name
      // isMale
      // age
      // 将person的isMale属性设为不可枚举之后
      // name
      // age

6.2. Object.keys(person)

  • 不检查原型链
  • 要求可枚举

返回一个数组,数组中的元素是对象中的可枚举的自有属性的名称,若在上一份代码中使用会输出["name", "age"]

6.3. Object.getOwnPropertyNames(person)

  • 不检查原型链
  • 不论是否可枚举

返回一数组,数组中的元素是对象中所有自有属性的名称,若在上一份代码中使用会输出["name", "isMale", "age"]

7. 附加

7.1. instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。 A instanceof B表面上是判断A对象是不是B构造函数构造出来的,实际上看A对象的原型链上 有没有B的原型 ,MDN说法是用于检测B构造函数的prototype是否出现在某个实例对象的原型链中

附上 developer.mozilla官方解释

7.2. 存取器属性

只能get方法,即只能读,只有set方法,即只能写,读取只写属性返回undefined
由这两个方法定义的属性称为存取器属性,而有其他简单的值定义的属性称为数据属性

存取器详情JavaScript学习笔记(一) 对象

7.3. 属性的特性

下面的文字源于 wsmrzx的博客 JavaScript学习笔记(一) 对象

除了在前文提到的可枚举性之外,对象的属性还有其它的特性,比如说可枚举性、可配置性等等
如果我们把数据属性的值看作是一个特性,那么数据属性总有具有四个特性,分别是

  • 值(value):数据属性的值,默认为 undefined
  • 可写性(writable):是否可以修改属性的值,默认为 true
  • 可枚举性(enumerable):是否可以通过 for/in 循环获取到值,默认为 true
  • 可配置性(configurable):是否可以删除和修改属性,默认为 true

如果我们把存取器属性的 getter 和 setter 方法也看作是特性,那么存取器属性同样具有四个特性,分别是

  • 读取(getter):在读取属性时调用的函数,默认为 undefined
  • 设置(setter):在设置属性时调用的函数,默认为 undefined
  • 可枚举性(enumerable):是否可以通过 for/in 循环获取到值,默认为 true
  • 可配置性(configurable):是否可以删除和修改属性,默认为 true

至此,我们对属性又有一个不同的理解:对象的属性是由一个名字(键)、 值 、特性 组成的

7.4. 附加

可使用Object.getOwnPropertyDescriptor(square, "x")来获取获取自有属性的描述符,代码控制台输出如下

      var square = {
         // 数据属性
         x: 5.0,
         // 存取器属性
         get area() { // 当读取存取器属性的值时,调用 getter 方法
            console.log('I am in getter')
            return Math.pow(this.x, 2)
         },
         set area(value) { // 当设置存取器属性的值时,调用 setter 方法
            console.log('I am in setter')
            this.x = Math.sqrt(value)
         }
      }
      square.area
      // I am in getter
      square.area = 16
      // I am in setter
      square.x
     >Object.getOwnPropertyDescriptor(square, 'area')
            // area为存取器属性
            {
               configurable: true,
               enumerable: true,
               get: ƒ area(),
               set: ƒ area(value)
            }

     >Object.getOwnPropertyDescriptor(square, 'x')
            // x为数据属性
         {
               configurable: true,
               enumerable: true,
               value: 4,
               writable: true
         }

还可以使用 Object.defineProperty() 方法设置属性的特性

注意最后的输出结果,因为get area()只读,而set area()只写,x又被我设置了不可枚举,所以只能输出area

8. 链式调用

jQuery的链是怎么实现的?

     
      var Tian = {
         smoke: function s() {
            console.log("I smoke!");
            // 此处默认返回undefined
            // 想要让他链式执行,需要返回this,实验发现返回Tian也可以,
            // 为什么用返回this的方法呢?
            return Tian;
         },
         drink: function () {
            console.log("I drink!");
            return Tian;
         },
         perm: function () {
            console.log("I perm!");
            return Tian;
         }
      }
      Tian.drink().perm().smoke();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值