深入学习JavaScript之对象

在JavaScript中,很多人都存在着一种误解,认为JavaScript中万物皆为对象,但是这其实是错误的。

1.1  语法

  在JavaScript中定义对象有两种语法。

  ①文字形式(声明形式):

    var  obj={

  key1:key1

  key2:key2

..........

}

②构造形式:

var obj=new  Object();

obj.key1=value;

  从适用性方面来说使用文字(声明)形式要比构造形式好得多,在文字形式中你可以一次性通过键值对添加多个属性"key1:key1,key2:key2",但是在构造形式中你只能逐个添加属性"obj.key1=key1;obj.key2=key2"。

1.2  类型

  对象是JavaScript的基础。

  在JavaScript中一共有六种主要数据类型(术语是:“语言类型”)

  • string
  • number
  • null
  • object
  • undefined
  • boolean

  简单的基本类型并不是对象,它们会在使用时被对象包装器包装成对象,这些简单的数据类型是:string、numbar、undefined、boolean以及null。其中null有时会被当做一个对象类型"typeof null"会返回一个"object",但是null并不是对象类型,它是一个基本类型,这只是JavaScript设计中的一个BUG(在底层机器中,操作或者对象都是二进制编码,在JavaScript中规定,当二进制前三位为0时,判定为object类型,null代表的是空对象,那么它的二进制编码就都是0,所以,null自然被判定为object类型

  在JavaScript中有许多特殊的对象子类型,我们称之为---------复杂基本类型

 函数就是对象的一个子类型(从技术上来说是"可调用的对象")。JavaScript中的函数是"一等公民","一等公民-------------值可以作为参数进行传递,可以从子程序中返回,可以赋给变量"。

  它们本质上与对象无差别,但是它们可以被调用。所以可以像操作其他对象一样操作函数,比如:将函数当做参数传入。

  函数是可调用的对象

 数组也是对象的一个类型,具备一些额外的行为。数组中内容的组织方式比一般的对象都要复杂一些。

内置对象

  JavaScript中还有一些对象子类型,通常被称为内置对象,有些内置对象的名称跟基本数据类型一样,但是它们之间的联系特别复杂。

  • String
  • Number
  • Object
  • Boolean
  • Funtion
  • Array
  • Date
  • RegExp
  • Error

   这些内置对象,从表现形式来说很像其他语言中的类型(type)或者是类(class),比如上述的内置对象String与Java中的String类。

  但是,在JavaScript中它们只是内置对象,这些内置对象可以配合关键字"new"发生构造调用,从而构造一个对应上述内置对象子类型的新对象。

 var string1="my name is zhu";
 typeof string1;        //string    基本数据类型
 string1 instanceof String;   //false
 var string2=new String("my name is ming");
 typeof string2;     //Object
 string2 instanceof String;  //true
 Object.prototype.toString.call(string2);   //[object String]

  这里要解释一下关键字"instanceof"的作用:二元操作符,判断左边例子是否为右边类(内置对象)的基本实例

  我们先是创建了一个基本数据类型:string的实例string,使用typeof判断string1的基本数据类型,结果:string。

  再使用new关键字配合内置对象"String",创建了子类型内置对象的实例:string2,使用typeof判断string2的基本数据类型,结果:Object。

  再使用关键字"instanceof"检查两个对象是否是"String"的实例。

  最后子类型在内部借用了Object中的toString方法。

  从以上结果来看,使用内置对象子类型搭配"new"可以创建一个基本数据类型为"Object"的对象。

  注意!!!原始值"my name is zhu"并不是一个对象,而是一个字面量,其值不可改变。如果要对这个值进行操作如:获取长度,插入某个字符等,必须转化为"String"对象。幸好,在JavaScript中,当你要对字符串字面量进行操作时,JavaScript会将此字面量转化成一个String对象。也就意味着你不需要显式的创建一个String对象

  var string1="my name is zhu";     //构造形式
     console.log(string1);
     console.log(string1.length);
     typeof string1;          //"string"

  这也就意味着能够使用构造形式的数据类型或字面量(string、number、boolean)创建构造形式对象时,JavaScript会自行转换该对象成内置对象子类型。

  但是因为"null"以及"undefined"没有构造形式,它们只有文字形式,因此它们不会被JavaScript转换成内置对象。相反的,Date有构造形式却没有文字形式。

  对于Objecrt、Array、Function以及RegExp来说它们都是对象不是字面量。在某些情况下,相比较文字形式,构造形式能够额外的增添一些选项。

因此,两种创建对象的用法推荐如下:

①先考虑语法简单的文字形式

②如果需要额外增添选项再考虑构造式

Error会在抛出异常时自动创建,也可以使用"new Error()"创建,一般来说没什么必要。

1.3  对象的属性---内容

  对象的内容是由一些存储在特定命名位置的(任意类型)的值组成的,我们称之为属性。

  虽说是内容,但是内容的存储位置却不在对象中。在引擎内部,值的存储位置是多种多样的,存储在对象中的只有名称,类似C语言中的指针,指向的是值的真正位置。

  要访问对象中的属性,有两种方法

  ①“属性访问”:通过"."操作符对对象的属性进行访问,例如:obj.a

  ②“键访问”:通过"[..]"操作符对对象属性进行访问,例如:obj["a"];

  var obj={
         a:2
     };
     obj.a;       //2        属性访问
     obj["a"];    //2        键访问

  属性访问和键访问都可以访问对象中的属性,它们可以访问同一个属性,得到同一个值。

  区别在于,属性访问只可以访问符合命名规范的属性,键访问可以访问任意的UTF-8/Unicode字符串。

  举个例子:要访问"super-fun!",你用"."操作符属性访问会出错"obj.super-fun!",原因是不符合标识符命名规范,但是你使用"[...]"操作符键访问就没有任何的问题,"obj["super-fun!"]"

  此外由于"[....]"使用字符串来访问属性,因此,可以用"[...]"在程序中构造这个字符串

举个例子

  var obj={
         a:2,
     };
     var idx;
     if(true){
         idx="a";   
     }
     console.log(obj[idx]);         //2   注意此处键访问,idx是不带有引号的引用

   在对象中,属性名永远是字符串。如果你使用string(字面量)以外的其他值作为属性名,那他首先会被转换成一个字符串。即使是数字也不例外,虽然在数组下标中使用的是数字,但是在对象属性中数字会被转换成字符串,所以千万不要搞混了对象和数组中的用法。

 var myObject={}; 
   //使用键访问进行构造属性
    myObject[true]="foo";
    myObject[3]="bar";
    myObject[myObject]="baz";


        //无论是怎样类型的字面量都转化成了字符串
    console.log(myObject["true"]);
    console.log(myObject["3"]);
    console.log(myObject["[object Object]"]);

我们在绝大多数情况下,采用的是"."操作符的属性访问,只有在某些特定情况下才会用"[...]"键访问,比如访问不符合规范的标识符

1.3.1  可计算的属性名

  "[...]"键访问的用处除了访问不符合规范的标识符之外,还能够通过表达式计算属性名。

  比如:myObject[prefix+name]。

  ES6中增加了可计算的属性名,在文字形式声明对象时,通过[   ]包裹一个表达式来当属性名

  var prefix="foo";
    var obj={
     [prefix+"bar"]:"hello",
     [prefix+"baz"]:"word"
    };
    console.log(obj["foobar"]);
    console.log(obj["foobaz"]);

  可计算属性名最常用的场景是ES6中的符号,符号是ES6中的一个新的基础类型,本身是一个字符串,包含着一个不透明无法预测的值,你不会用到这个符号的值,你只会用到这个值的名称。

1.3.2  属性与方法

  当一个对象中的属性是一个函数时,绝大多数人都喜欢称这个函数为方法,这个说法有什么问题吗?答案是,之前我们说过函数是一个可被调用的对象,方法的定义是:属于某一个对象的函数,如此一来称呼函数为方法有点不太恰当。

  函数如果是一个对象的属性,那么我们可以称之为属性访问。

  这时候你就要疑惑了,函数中不是存在着"this"关键字吗?这不正是函数是方法最有利的证明吗?

  确实,有些函数具有this引用,这些this确实会指向调用位置的对象引用。但是这种用法从本质上来说并没有把函数变成一个方法,因为"this"是根据调用位置动态绑定的,所以函数与对象的关系最多也只能说是间接关系

  函数在对象中,它的返回值是一个函数。但是,无论返回值是什么类型,每次访问对象的属性就是属性访问。如果它对的返回值是一个函数,那么也不能叫做方法,属性返回的函数和其他函数没有任何区别,除了隐式绑定this的指向以外。

  举个例子

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

  somefoo以及obj.somefoo是对foo()函数的不同引用,输出是一样的。如果foo()函数中存在着一个this,那么当somefoo()引用foo()时,this指向全局变量,obj.foo引用foo()的话,this指向obj内部。如果不明白的可以看我的https://blog.csdn.net/qq_41889956/article/details/83386111这篇文章。

  那么当在对象中定义一个函数时,这个函数会不会成为方法呢?

  看个例子

 var obj={
     foo:function () {
         console.log("foo");
     }
 };
 var somefoo=obj.foo;
 somefoo();           //foo  funciton (){...}
 obj.foo();           //foo  funciton (){...}

  可以看出来,在一个对象的文字形式中创建一个函数,这个函数也不能称之为这个对象的方法。

1.3.3  数组

  数组支持用[...]进行访问,数组有一套更加结构化的值存储机制(值的类型不限制)。数组期望的是数组下标,数组下标只能够为整数,这个整数称之为索引,比如:0或者5

  var myArray=["baz","bar",3];
        myArray[0];    //baz
        myArray[1];    //bar
        myArray[2];    //3
        myArray.length;    //3

数组也是对象,虽然每个数组下标是整数,但是你也可以给它添加属性。

 var myArray=["baz","bar",3];
        myArray.bax="bax";    //添加了bax这一个属性
        console.log(myArray.bax);
     console.log(myArray.length);    //3  bax没有算进数组索引中

  可以看到尽管添加了命名属性(无论是"."操作符还是"[...]"),但是数组的长度却没有发生任何变化。

  你可以将数组看成是一个对象来对待,但是我们不支持这种做法,因为在JavaScript中对,对象,数组都进行了优化。

  最好用对象来存放键值对,用数组来存储数值下标/值对。

  注意如果你通过"[...]"来向数组添加属性时,你添加的属性名是一个数字,那它会变成一个数组下标(因此会修改数组内容而不是添加属性)

 var myArray=["baz","bar",3];
       console.log(myArray[2]);     //3
       console.log(myArray.length)   //3
       myArray["2"]="foo";
       console.log(myArray.length);    //3
       console.log(myArray[2]);      //foo
   

  可以看出,myArray[2]被新添加的myArray["2"]替代,原本的数组长度为3,添加属性后的数组长度也为3。那么原本的myArray[2]=3被修改成了myArray[2]="foo"。

1.3.4  复制对象

  在某些情况下,我们需要赋值对象,但是复制对象往往存在着很大的问题。

  赋值对象分为两种:

①浅复制:对象中属性引用依旧为属性引用,属性的值为字面量时,新的属性掩盖旧的属性

②深复制:不止会复制属性,还会复制引用的函数。

  这么说起来有点难以理解,让我们来看看代码

 function anotherFunction() {
          /..../
      }
      var anotherObject={
          c:true
      };
      var anotherArray=[];
      var myObject={
          a:2,
          b:anotherObject,    //引用,不是复制
          c:anotherArray,      //同样是引用
          d:anotherFunction
      }
    anotherArray.push(anotherObject,myObject);

  如何表示myObject的复制呢?

  首先,我们先判断它是浅复制还是深复制。

  对于浅复制来说,复制出的新对象中"a"的值会复制旧对象中"a"的值,也就是2,但是新对象中的"b  c  d"其实就是三个引用,它们的作用跟旧对象的属性是一样的。

  对于深复制来说,这个就很复杂了,它复制的对象除了myObject之外还会复制anotherObject、anotherArray。这时就出问题了,在代码的最后一行"anotherArray.push(antherObject,myObject)"又再次引用了myObject,于是又会复制这一个对象,在这一个对象中我们又需要复制anotherArray,由此形成了死循环。

  我们是应当检测循环并终止循环(不复制深层元素)?还是应当直接报错或者是选择其他方法?

  除此之外,我们还不能确定“复制”一个函数意味着什么,有些人通过"toString"来序列化一个函数的源代码(但是结果取决于JavaScript的具体实现,不同的引擎对于不同类型的函数处理方式并不完全相同)。

  那么如何解决这一个棘手的问题呢?许多的JavaScript框架都提出了自己的解决方法,但是JavaScript应当采取哪种方法作为标准呢?在很长一段时间内这个问题都没有答案。

  对于JSON安全(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值一模一样的对象)的对象来说有一种巧妙的方法。  

var newObj=JSON.parse(JSON.stringify(someObj));

  当然,这种方法需要保证对象是JSON安全的,所以只能适用部分情况。

  相比较于深复制,浅复制要简单得多。在ES6中定义了Object.assign(...)方法来实现浅复制。

  Object.assign方法的第一个参数是目标对象,之后可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举的自有键,并把它们复制(使用=操作法)到目标对象,最后返回目标对象。

  接着上面的代码:

var newObj=Object.assign({},myObject);
      newObj.a;   //2
      newObj.b===anotherObject;   //true
      newObj.c===anotherArray;    //true
      newObj.d===anotherFunction;   //true

  注意因为是使用=操作符进行复制,源对象属性的一些特性(比如writable)是不会被复制到目标对象的。

1.3.5  属性描述符

   在ES5之前,JavaScript中没有什么方法能够检测属性特性,比如判断属性是否可写

  但是自ES5开始,所有的属性都具有了属性描述符

  属性特性符又称“数据描述符”:描述属性的某些特性,例如:value、writable、enumeration

 思考以下代码:

 var myObject={
         a:2
     };
     console.log(Object.getOwnPropertyDescriptor(myObject,"a"));
     // {
     // value: 2,       值:2
     // writable: true,   可写:true
     // enumerable: true,  可枚举:true
     // configurable: true   可配置:true
     // }

  上述代码中我们创建了一个myObject对象,其中由一个属性"a=2",在ES5以上的版本中无论是任何的属性都带有属性描述符,我们使用"Object.getOwnPropertyDescriptor(...)"得到属性默认的属性描述符。

  "Object.getOwnPropertyDescriptor(对象,"属性")"中传入的第一个参数为想要了解的对象,第二个参数为属性,为想要了解的属性。例如本例中,我们想要了解"myObject"这个对象的"a"属性的属性特性符有哪些。

  在创建普通属性时,普通属性的属性特性符是默认值(writable:true、enumerable:true、configuration:true),但是你可以使用Objcet.defineProperty(对象,"属性名",修改体)来修改属性特性符

var myObject={};
    Object.defineProperty(myObject,"a",{
        value:2,
        writable:false,
        enumerable:false,
        configurable:true
    });
    console.log(myObject.a);           //2
    console.log(Object.getOwnPropertyDescriptor(myObject,"a"));  //{value:2,writable:false,enumerable:false,configurable:falase}

  利用Object.defineProperty(...)可以为对象添加属性,并修改属性特性符,但是正在一般情况下你不会使用此方法添加属性,除非你想要修改属性特性符。

  

下面介绍各个属性特性符的作用

①writable

  writable是决定属性是否可被修改

  writable:true----可修改

  writable:false-----不可修改

 var myOcject={
        a:2
    };
    Object.defineProperty(myOcject,"a",{
        writable:false
    });
    myOcject.a=3;            //此处想要修改属性“a”值为3
    console.log(myOcject.a);       //2   修改失败,因为writable为false

  我们尝试使用"myObject.a=3"修改"a"的值,但是由于"writable:false",所以我们从输出可以看出,我们修改失败。

  但是!!!在严格模式下会出错,因为它会提示你修改了一个无法修改的属性

"use strict";
    var myOcject={
        a:2
    };
    Object.defineProperty(myOcject,"a",{
        writable:false
    });
    myOcject.a=3;
    console.log(myOcject.a);  //TypeError

运行结果

②configurable

  configurable决定属性是否能被配置,配置即为修改属性的属性特性符

  configurable:true-------可配置

  configurable:false------不可被配置

  var myObject={
        a:2
    };
    Object.defineProperty(myObject,"a",{
        writable:true,
        enumerable:true,
        configurable:false
    });
    myObject.a=3;
    console.log(myObject.a);    //3
    Object.defineProperty(myObject,"a",{
        writable:true,
        enumerable:false,
        configurable:true
    });  //TypeError

  从上述结果可以看出,属性特性符"configurable:false"时,"myObject.a=3"赋值成功,而使用Object.defineProperty(...)修改属性特性符失败抛出错误。证明"configurable"是决定属性特性符能否被配置

  无论是处在严格模式下或者是非严格模式下,当你尝试修改一个不可配置的属性特性符都会出错。

  把"configurable"修改是单向的无法撤销!!!

  此处有一个小细节,及时你把"configurable"修改为false,"writable"的值依然可以从true变成false,但是无法由false变成true

 

当configurable:false时,你除了无法修改属性特性符,你还无法删除属性!!!

  var myObject={
        a:2
    };
   console.log(myObject.a);  //2
    delete myObject.a;   //删除myObject.a
    console.log(myObject.a);  //undefined   删除成功

    Object.defineProperty(myObject,"a",{
    value:3,
        configurable:false
    });
    console.log(myObject.a);  //3
    delete myObject.a;        //删除myObject.a    
    console.log(myObject.a)  //3   删除失败

  在我们没有将"configurable"修改为"false"时,这时的"configurable"默认为true,我们尝试删除"a"属性,成功。当我们将"configurable"修改为false后,尝试修改失败。

  因为此时属性是不可被修改的。

③enumerable

  enumerable控制的是属性是否会出现在对象的属性枚举中比如说"for..in"循环

  enumerable=true-------该属性能够出现在对象的枚举中

  enumerable=false------该属性不能够出现在对象的枚举中

var myObject={
    c:1
};
Object.defineProperty(myObject,"a",{
    value:2,
    enumerable:true
});
console.log(myObject.a);   //2
Object.defineProperty(myObject,"b",{
    value:3,
    enumerable:false
});
console.log(myObject.b);   //3
  console.log("a" in myObject);   //true  判断a是否在myObject中
  console.log("b" in myObject);   //true  判断b是否在myObject中
  for(var k in myObject){          //属性存在于对象在中就会被输出
      console.log(k,myObject[k]);    //a:2  c:1  b没有出现
  };

    从结果我们可以看出,属性"b"的属性描述符"enumerable:false"时,在for...in循环中,无法发现"b"。所以枚举最通俗的说法就是对象的遍历,可枚举就是“能否出现在对象的遍历中”。

  此处的for...in循环并不适合用在数组中,因为这种枚举(遍历)不仅会包含数组索引还会包含所有可枚举属性。在遍历数组时,最好还是使用简单的for循环。

var myObject=[1,2,3];
  myObject.a="a";         //数组中添加的可枚举属性
  for(var k in myObject){    //遍历数组
      console.log(k,myObject[k]);    //0:1  1:2  2:3  a:a  本意为遍历数组的索引,现在变成了遍历数组所有可枚举属性
  }
  for(var i=0;i<myObject.length;i++){
      console.log(i,myObject[i]);       //0:1 1:2 2:3    使用普通for遍历正常数组
  }

 

也可以通过另一种方式判断是否可枚举,那就是"Object.propertyIsEnumerable(...)"

  var myObjct={};
            Object.defineProperty(myObjct,"a",{
                value:2,
                enumerable:true
            });
            Object.defineProperty(myObjct,"b",{
                value:3,
                enumerable:false
            });
            console.log(myObject.propertyIsEnumerable("a"));   //true
            console.log(myObject.propertyIsEnumerable("b"));    //false
            console.log(Object.keys(myObjct));              //["a"]
            console.log(Object.getOwnPropertyNames(myObjct));//["a"]  ["b"]

propertyIsEnumerable(..)会检查给定的属性名是否直接存于对象中(而不是原型链中),并且满足"enumerable:false"

"Object.keys(...)"会返回一个数组,包含所有可枚举的属性,"Object.getOwnPropertyNames(...)"也会返回一个数组,包含所有属性(无论可不可枚举)。这两个函数都只会在对象中查找,而不会设计到原型链。

  

1.3.6  不变性

   在某种情况下,你会希望对象或者属性不可被改变,在ES5中有很多方法实现。

  很多方法创建的都是浅不变形,也就是说它们只会影响目标对象和它们的直接属性。如果目标对象引用了其他对象(数组,函数,对象)的话,其他对象的内容不受影响,但仍是可变的。

举个例子:

  myObject.foo;   //[1,2,3]
  myObject.foo.push(4);
  myObject.foo;  //[1,2,3,4]

  假设代码中的"myObject"已经创建且不可改变,但是我了保护它里面的可调用对象"foo",我们还需要用以下方法让"foo"也不变。

①对象常量

  在上一节中,我们学习了属性描述符,我们可以利用属性描述符,让对象属性不可写,不可重定义,不可删除,成为一个真正意义上的对象属性常量。

  为属性添加"writable:false   configurable:false"

 var myOdject={};
  Object.defineProperty(myOdject,"a",{
     value:2,
      writable:false,
      configurable:false
  });
  console.log(myOdject.a);    //2
  myOdject.a=3;
  console.log(myOdject.a);    //2  对a修改无效
  delete myOdject.a;
  console.log(myOdject.a);    //2

 可以看出使用"writable:false  configurable:false"之后,"a"属性不可被重写,也不可被删除。

②禁止扩展

  如果你希望一个已经创建的对象不能够添加属性且保留原来属性,那么就可以用到"Object.preventExtensions(.....)"

  Object.preventExtentions(对象)

 var myObject={
      a:2
  };
  Object.preventExtensions(myObject);    //禁止myObject对象添加新属性
    myObject.b=3;
    console.log(myObject.b);    //undefined

  在非严格模式下,创建"b"属性会出错,在严格模式下会抛出"TypeError"错误

③密封

  密封是指:密封一个对象,使它不能够添加属性,且保留的属性也不可删除,但是属性值可以修改

  "Object.seal(...)"可以完成这个功能,从功能上看,"Object.seal(...)"就像是结合了前两个功能(常量以及禁止拓展),这种说法也不是很对。

  但是"Object.seal(...)"方法的具体实现是:对在一个传入对象中调用"Object.preventExtensions(....)"再修改属性特性符"configurable:false"。正因如此,可以修改对象属性的值(因为没有修改"writable")

var myObject={
     a:2
 };
 Object.seal(myObject);    //密封对象
 console.log(myObject.a);   //2
 Object.defineProperty(myObject,"a",{
     enumerable:false
 });       //TypeError  尝试修改属性特性符失败,证明configurable:false

 myObject.b=3;
    console.log(myObject.b);  //undefined  尝试添加属性失败,证明Object.preventExtensions(MyObject)
    myObject.a=4;
   console.log(myObject.a);   //尝试修改

④冻结

  "Object.freeze(...)"会创建冻结一个对象,这个对象实际上是在  密封(Object.seal(...))  的基础上添加"writable:false",真正做到了一个属性无法添加三处属性,也无法修改属性的值。

  此方法是应用在一个对象上最高的不变性。它会禁止对于对象本身及其任意直接属性的修改(不过这个对象引用其他对象不会受到影响)

  你可以“深度冻结”一个对象,具体怎么做呢?遍历一个对象,将每个对象添加"Object.freeze(...)",如此一来这个对象的属性,既不能被修改,也不能被删除,重写,更不能添加属性,属性特性符也不能被修改。但是很有可能因此冻结了其他的共享对象

1.3.7  [[Get]]

  在我们访问对象中的属性时,其实是发生了很多事情的。

 var myObject={
       a:2
   };
   console.log(myObject.a);   //2

    我们是如何查找对象的属性的呢?通常的一种看法是,在对象中查找属性为"a"的属性,这种说法不全对。

  在语言规范中,myObject.a在myObject中,实际上是实行了[[Get]]操作(这个操作有点类似函数调用时的[[Get]]()  )。对象内置的[[Get]]操作首先在对象内查找是否存在相同名称的属性,如果找到的话就返回这个属性。

  找不到的话,按照[[Get]]算法的定义,会到“”原型链”中查找-------------其实就是遍历"Prototype"链,也就是遍历原型链。

  如果仍找不到的话,则返回"undefined"。

 var myObject={
       a:2
   };
   console.log(myObject.b);   //undefined

  让我们来分析一下"myObject.b"这一条语句执行时发生的事情。

①"myObject.b"开始执行,这时我们告诉引擎,我们需要对象"myObject"中名为"b"的属性。

②引擎收到命令,开始执行[[Get]]操作,开始在对象"myObject"中查找名为"b"的属性。

③在对象"myObject"中不存在名为"b"的属性,于是[[Get]]算法让引擎遍历相关的"原型链"又称"prototype"。

④原型链中不存在名为"b"的属性,这时返回值"undefined"

注意,很多人会把变量和对象属性弄混,我们之前讲过"LHS"以及"RHS",这是查找变量的两种方式。当我们在词法作用域中查找变量时会使用"LHS"或者"RHS"。[[Get]]是查找对象属性的,与变量没有关系

 var myObject={
       a:2
   };
   console.log(myObject.b);   //[[Get]]操作   undefined
    console.log(b);    //ReferenceError   这里是RHS查找

这时会出现一个问题,便是当我们访问一个对象的属性,该属性的值为"undefined",使用[[Get]]操作查找不到属性时返回值同样是"undefined",那么我们如何确定该值到底是存在还是不存在呢?

例如以下的代码

  var myObject={
       a:undefined
   };
   console.log(myObject.a);   //undefined
    console.log(myObject.b);    //undefined

  在1.3.10中我们介绍了如何区分这两种情况。

1.3.8  [[Put]]

  既然存在着[[Get]]得到属性值,那么也会存在[[Put]]修改属性值。很多人认为修改属性值(包括给属性赋值以及创建属性)时会触发[[Put]]操作,但是实际情况非常复杂。

  具体来说[[Put]]被触发时,实际的行动取决于多个元素,最重要的隐式是:对象是否已经存在这个属性

  如果存在这个属性,[[Put]]算法大致会检查以下内容。

①属性是否是访问描述符?如果是且存在"setter"就调用"setter"。

②属性的属性描述符"writable"是否是"false",是的话,在非严格模式下修改失败,在严格模式下,会返回"TypeError"(因为严格模式在writable:false时禁止修改属性值

③如果都不是,则将该值赋给该属性

如果不存在此属性,[[Put]]操作更加复杂,将会同[[Get]]一样涉及到原型链

1.3.9  getter和setter

  对象默认的[[Get]]和[[Put]]操作可以分别控制属性值的获取和设置。

  在ES5中可以使用getter和setter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。

getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。

  当你给一个属性同时定义getter和setter时,这个属性称为"访问描述符"(与属性描述符相对)。对于访问描述符来说,JavaScript会忽略它们的"value"和"writable"特性,取而代之的是关心set和get(还有enumerable和configurable)特性。

  在这里所谓的访问描述符指的是在对象中,使用"getter"和"setter"定义的属性。

简单来说对象中的属性分成两类,一类是使用键值对的属性描述符,一类是使用"getter""setter"的访问描述符。

 var myObject={
      get a(){   //给a定义一个getter  这个a就是访问描述符
          return 2;
      },
      c:3          //属性描述符
  };
  Object.defineProperty(myObject,"b",{    //添加访问描述符b
      get function(){   //给b定义一个getter
          return this.a*2;
      },
      enumerable:true     //保证b能够在myObject中创建
  });
  console.log(myObject.a);   //2
  console.log(myObject.b);   //4

  我们来解析下,当我们访问,访问描述符的时候发生了什么。

  我们在对象"myObject"中使用"get a(){return 2}"定义了一个访问描述符"a",当我们访问"a"时,并不会像之前那样调用[[Get]]去处理,而是调用一个隐藏函数"getter"这个函数会返回一个值,这个值就是该访问描述符的值。

  同理的在"Object.definedProperty(...)"中也可以创建访问描述符,并且使用"getter""setter"定义访问描述符。

 var myObject={
     get a(){
         return 2;
     }
 };
 console.log(myObject.a);   //2
 myObject.a=3;
 console.log(myObject.a);   //2

  由于我们只定义了"a"的"getter",所以对"a"的值进行设置时,"set"操作会忽略赋值操作,不会抛出错误。而且即使有合法的"setter",由于我们自定义的"getter"只会返回2,所以"set"是没有意义的。

  所以为了让属性更加合理,还应当定义"setter","setter"操作会覆盖单个属性默认的[[Put]](也被成为赋值操作)

  通常来说"getter""setter"是成对出现的

var myObject={
    get a(){
        return this._a_
    },
    set a(val){
        this._a_=val;
    }
};
myObject.a=2;
console.log(myObject.a);   //2

在本例中的"_a_"只是一个变量名而已没有特殊的含义,在此程序中的作用是存储传入a的值。

1.3.10  存在性

  在前面我们提到过,当使用[[Get]]查询不到属性时会返回"undefined",如果[[Get]]查询到的属

性值就是"undefined"的话,我们该如何区分这个属性到底是存在还是不存在呢?

  ①通过"in"查找

  我们可以通过"in"关键字判断该属性是否存在于对象中

var myObject={
    a:undefined
};
console.log(myObject.a);        //undefined
console.log(myObject.b);        //undefined
console.log("a" in myObject);   //true
console.log("b" in myObject);   //false

  通过结果我们可以看出,("属性名"  in  对象)这一行代码1,可以检测出属性是否存在于对象中。

  in关键字的原理是:检查属性是否在对象及其"prototype"原型链中。

注意!!!in看起来像是检查某值是否存在,但是它只是在检查属性名,这点在数组中尤为重要,例如:"3  in  [1,2,3]",返回值是"false",为什么呢?因为在数组中属性名是"0  1  2"没有"3"

②hasOwnProperty(...)

我们可以通过"hasOwnProperty(...)"方法来检测。

var myObject={
    a:undefined
};
console.log(myObject.a);        //undefined
console.log(myObject.b);        //undefined
console.log(myObject.hasOwnProperty("a"));   //true
console.log(myObject.hasOwnProperty("b"));   //false

  "hasOwnProperty(...)"与"in"不同,它只会检查属性是否在对象中,不会检查"prototype"原型链。

  所有的对象都可以通过对于"Object.prototype"的委托(原型链内容)来访问"hasOwnProperty",但是有的对象可能没有连接到"Object.prototype"(通过Object.create(null)来创建)。在这种情况下"hasOwnProperty"就会失败

  这时可以借助一个更加强力的方法来进行判断:"Object.prototype.hasOwnProperty.call(myObject,"a")"。它借助"call"将"hasOwnProperty"显式绑定到"myObject"上。

1.4  遍历

for...in循环只能够遍历数组的属性(会在对象及其相关的"prototype"原型链中查找),而且是可枚举的属性,而不能够遍历数组的值,那么我们想要遍历属性的值该怎么做呢?

  数组可以通过基本的for循环遍历数组属性的值

  var myObject=[1,2,3];
        myObject.a="a"
          for(var i=0;i<myObject.length;i++){     //遍历数组
              console.log(i,myObject[i]);   //0:1  1:2 2:3 没有a属性
          };

 但是这实际上不是在遍历数组,而是在遍历数组的下标指向值。

如何解决和一个问题呢?

好在ES5中增加了专门用于数组遍历的迭代器,用以辅助数组遍历,每个迭代器都能接受一个回调函数并把它应用在数组的每个元素上,这几个迭代器唯一的区别就是对回调函数的处理不同。

①forEach(...)会遍历数组中的所有值并忽略回调函数的返回值。

 var myObject=[1,2,3];
          myObject.forEach(function (element) {
              console.log(element);  //1,2,3 
          })

②every(...)会一直运行直到回调函数返回"false"(或者“假”值)。此回调函数有点像"break"处理,满足条件之后跳出

③some(...)会一直运行到回调函数返回"true"(或者“真”值)。此回调函数有点像"break"处理,满足条件之后跳出

那么如何遍历数组值而不是数组下标呢?

在ES6中增加了一种用来遍历数组的"for..of"循环语法(如果对象定义了迭代器也可以遍历对象)

    var myObject=[1,2,3];
          myObject.a="a";
         for (var v of myObject){
             console.log(v);    //1  2  3
         }

  下面我们来介绍一下"for..of"对象的原理

  "for..of"首先会向对象请求一个迭代器对象,然后通过迭代器对象的"next()"方法来遍历是所有返回值。

  数组中有内置的"@@iterator",因此"for...of"可以直接应用在数组上,我们使用内置的"@@iterator"来看看它是如何工作的?

   var myObject=[1,2,3];
        var it=myObject[Symbol.iterator]();
      console.log(it.next());   //{value: 1, done: false}
          console.log(it.next());  //{value: 2, done: false}
          console.log(it.next());   //{value: 3, done: false}
          console.log(it.next());   //{value: undefined, done: true}

  使用迭代器的"next(...)"方法会返回一串形如“{value:1,done:false}”的值,其中value是当前遍历的值。done是一个布尔值,表示事都还有可遍历的值。

  这时你会感到奇怪,在"value:3"时,"done:false"。这是否代表了还存在下个值呢?并不是,而是你必须要在调用一次"next(...)"得到"done:true"才能完成遍历。

我们使用ES6中的符号symbol.iterator来获取对象的@@iterator内部属性。这里的symbol是符号“也就是我们之前讲过的ES6中的基础类型,是一个字符串,包含着一个不透明无法预测的值,你不会使用到这个值,你只会使用到这个值的名称”。

  引用类似iterator的特殊属性时要使用符号名,而不是符号所包含的值,@@iterator开起来很像一个对象,但是并不是迭代器对象,而是一个返回迭代器对象的函数--------------这点特别关键

  注意!!!在数组中才有内置的@@iterator,普通对象中没有,但是你可以手动给普通对象添加@@iterator,用以实现for...of循环

  var myObject={
           a:2,
           b:3
       };
       Object.defineProperty(myObject,Symbol.iterator,{      //为普通对象添加特殊属性符号Symbol.iterator
          enumerable:false,
           writable:false,
           value:function () {
               var o=this;     //指向当前对象
               var idx=0;       //判断done
               var ks=Object.keys(o);    //keys(...)会返回一个可枚举属性的数组,令ks=这个数组
               return {                  //iterator会返回一个迭代器对象的函数
                   next:function () {    //定义itertor的next()方法
                       return{
                           value:o[ks[idx++]],     //输出该对象当前的值,令idx加1换下个对象
                           done:(idx>ks.length)        //判断idx,大于ks.length的话,输出true。
                       }
                   }
               }
           }
       });
       //手动调用
    var it=myObject[Symbol.iterator]();
    it.next();
    it.next();
    it.next();
    //for...of调用
    for(var v of myObject){
        console.log(v);
    }

  看起来为一个普通对象创建一个特殊属性"iterator"非常复杂,但是我们仔细解刨的话,会发现非常简单。

①创建对象"myObject"

②跟其他普通属性一样,使用"Object.definedProperty(...)"创建特殊属性,但是注意这里的属性名只能是不带引号的Symbol.iterator。

③属性描述符"enumerable:false    wratable:false",特殊属性:iterator禁止被改写,最好不枚举。

④因为"iterator"的返回值是一个函数,所以value:  funcction(){...}

⑤在function中进行数据处理

⑥在function中定义一个return{...},这里面存放next(..)函数处理。

当然你也可以不在'Object.definedProperty(...)"中定义,而是在定义对象中直接声明键值对。

var  myObject={

a:2,

b:3,

[Symbol.iterator]:

function(){.....}

for...of循环每一次调用"myObject"迭代器对象的next()方法时,内部的指针就会向前移动并返回对象属性列表的下一个值。

 

1.4.1  更高级别的遍历(用户自定义特殊属性)

  你自己定义的对象来说,结合了for...of循环和自定义迭代器可以组成非常强大的对象操作工具。

举个例子,我们可以创建一个“无限”迭代器,它永远不会“结束”,每次都会返回一个新值(比如随机数、递增值,唯一表示符等),别在for...of中使用这样的迭代器,你的程序将会被挂起

 var randoms = {            //构建随机生成的迭代器
        [Symbol.iterator]: function() {
            return {
                next: function() {
                    return { value: Math.random() };     //随机生成数,每次访问random都调用一次Math.random()
                }
            };
        }
    };
    var randoms_pool = [];
    for (var n of randoms) {
        randoms_pool.push( n );   //随机生成的数n,被随机添加到random_pool中
// 防止无限运行!
        if (randoms_pool.length === 100) break;
    }

  这个迭代器将会随机生成一个新值,为什么呢?因为在每次调用random时,都会调用一次Math.random()。将源源不断的产生新值。

总结:创建对象有两种方式,一种是文字(声明)形式(var a={b:1,....}),一种是构造形式(var a=new String()),一般来说我们会使用文字形式。

      许多人认为JavasScript中万物皆为对象,这种观点是错误的,对象只是JavaScript基础类型中的一种。对象有包括function在内的子类型,不同子类型具有不同的行为,比如说[Object.Array]表示这是对象的子类型数组。

  对象就是键值对的集合,可以通过属性访问"myObject.a"访问属性,也可以通过键访问"myObject.["a"]"来访问属性。无论哪种方式访问属性都会调用[[Get]]操作(设置属性时是[[Put]]),它会在对象和原型链中查找属性,找不到时返回"undefined",区分属性是否存在可以使用"in"和"hasOwnProperty"关键字,这两种方法的区别在于前者会查找原型链,后者则不会。

  创建属性时,会默认创建属性特性符,且值都为"true",属性特性符又称“数据描述符”,有

wirtable:属性是够可写  

enumerable:属性是否被枚举(for...in循环)

configurable:属性是否被配置(单向的当为false以后就再也改不回,且属性不可被删除)

修改属性特性符(也可创建属性):Object.definedProperty(对象,“属性”,{修改函数体})。

查看属性特性符:Object.getOwnPropertyDescriptor(对象,属性)。

此外还可以通过:

Object.preventExtensions(...)禁止扩展对象(不能添加新属性)

Object.seal(...)密封对象(不能添加属性,且无法删除属性,但属性值无法修改)

Object.freeze(...)冻结对象(在密封的操作下且无法修改属性值)。

与属性特性符相对的便是"访问描述符"了,这个访问描述符与普通属性不同,它是通过"getter"函数获取属性值,"setter"函数设置属性值

 

  你要想遍历一个对象得到属性的值,数组的话,你可以使用ES6中的for...of循环。普通对象的话,要想使用for...of循环可以自建"iterator"符号及迭代器,这能组成很强大的效果。

for...of循环会寻找内置的"iterator"或者自定义的"iterator"调用内部的next()来遍历对象。

猜你喜欢

转载自blog.csdn.net/qq_41889956/article/details/83716216
今日推荐