【前端进阶】 全面剖析JS之“Object” 看到最后发现你原来枪里没有子弹

1.1 语法

对象有两种形式定义:
1. 声明(文字)形式
2. 构造形式

1.声明形式
	var myObj={
		key:value,
		//....
	}

2.构造形式
	var myObj=new Object();
	myObj.key=value;

构造形式和文字形式生成的对象是一样的。唯一的区别是:在文字声明种你可以添加多个键/值对,但在构造形式中,你必须逐个添加属性。
注意: 通常使用内置对象的构造函数声明对象往往影响性能。

1.2 类型

在javascript中,一共有六种主要类型:

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

注意,简单的基本类型(string,number,boolean,null,undefined)本身并不是对象。null有时会被当做对象类型,但实际上这是语言本身的一个bug。在javascript中,不同对象在底层都表示为二进制,二进制前三位都为0的话会被判定为object类型,null的二进制表示全为0,所以执行typeof时会返回object类型。实际上,null只是一个基本类型。

注意: 函数与数组是对象的一个子类型,因此用typeof判定类型时都返回object。

1.3 内置对象

javaScript中的对象子类型,被称为“内置对象”。

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

这些内置对象可以运用new操作符来创建一个对象。
举例:

var str="I'm a string";
typeof str;//"string"
str instanceof String;//false

var strObject=new String("I'm a string");
typeof strObject;//"object"
strObject instanceof String;//true

str=“I’m a string”;并不是一个对象,它只是一个字面量。如果在字面量上执行一些操作,比如获取高度、访问其中某个字符等,那需要转化为String对象。不过,幸运的是:在必要时,语言会自动把字符串字面量转换成一个String对象,也就是说你并不需要显示创建一个对象。应记住少用内置对象函数来创造对象。

看下面代码:

var str="I am a string";
console.log(str.length);//13
console.log(str.charAt(3);//"m"

我们可以直接在字符串字面量上访问属性或方法。之所以能这么做,是因为引擎自动把字面量转换成String对象。

注意: null和undefined没有相应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式。

注意: 对于Object、Array、Function和RegExp来说,无论使用文字形式还是构造函数形式,它们都是对象,不是字面量。这在使用typeof时都是object。

1.4对象属性的value

我们需要知道的是,属性的value并不是存储在对象的内部。在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器的内部。存储在对象容器内部的是这些属性的名称,它们就像指针一样,指向这些值真正的存储位置。
看下面代码:

声明:
var myObject={};
myObject[true]="foo";
myObject[3]="bar";


调用:
myObject["true"];//"foo"
myObject["3"];//"bar"

从上面可以看出,在对象中,属性名永远是字符串。如果你是用string意外的其它值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串。

数组也是对象,所以即使每个下标都是整数,你仍然可以给数组添加属性:

var myArray=["foo","bar",4];
myArray.baz="baz";
myArray.length;//3
myArray.baz;//"baz"

可以看到虽然添加了命名属性(无论是通过.语法还是[]语法),数组的length值并未发生变化。在这里不建议大家在数组中使用属性。最好是对象使用来存储键/值对,只用数组来存储数值下标/值对。

注意: 如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值的下标(因此会修改数组的内容,而不是添加一个属性)。
看下面代码:

var myArray=["foo",42,"baz"];
myArray["2"]="bar";
myArray[2];//"bar",第二行代码修改了数组的第三个元素值

1.5 复制对象

复制对象即是拷贝对象,拷贝有深拷贝、浅拷贝之分。并不是使用简单的“=”操作符即可。由于这个相对复杂,我已在另一篇博客详细解释,有兴趣可以查看。复制对象是对象的重点,务必掌握。

1.6 属性描述符

var obj={
  a:"foo"
}

我们学习不能只看表面,上面就是一个简单的带有属性a的对象,这看上去谁都懂,但你真的了解每个属性吗?
带你了解属性内在东西。

语法:Object.getOwnPropertyDescriptor(obj,"某个属性")

查看对象的a属性:

var obj={
  a:"foo"
}
console.log(Object.getOwnPropertyDescriptor(obj,"a"));

打印结果:
在这里插入图片描述
如图所见,这个普通的对象属性对应的属性描述符可不仅仅只有value:“foo”,它还有另外三个特征:

  • writable:可写
  • enumerable:可枚举
  • configurable:可配置

在创建普通属性时,属性描述符会使用默认值。我们也可以使用下面语法来添加一个新属性或修改一个已有属性(修改的前提是该属性configurable:true)。

语法:Object.defineProperty()

举例:添加一个属性b,使之不能在被修改值,总是55

Object.defineProperty(obj,"b",{
	value:55,
	writable:false,
	configurable:true,
	enumerable:true
	})

一般我们都不会使用这总方式,除非我们想修改属性描述符。

1.6.1 writable

决定是否可以修改属性的值
看下面代码:

 var obj={
   a:"foo"
 }
 Object.defineProperty(obj,"a",{
      value:"foo",
      writable:false,
      configurable:true,
      enumerable:true
    })
    obj.a="FOO";//尝试修改属性a

看运行结果:
在这里插入图片描述
TypeError: 表示我们不可以修改一个不可写的属性。

1.6.2 configurable

只要属性是可配置的,就可以使用Object.defineProperty()来修改属性描述符。一旦configurable:false,则不再能使用它修改属性描述符的任意一个值。

   var obj={
     a:"foo"
   }
  
    obj.a="FOO";
    console.log(obj.a);
    
    Object.defineProperty(obj,"a",{
      value:"FOO",
      writable:true,
      configurable:false,
      enumerable:true
    })
    obj.a="AAA";
   console.log(obj.a);
   
   Object.defineProperty(obj,"a",{
    value:"FOO",
    writable:true,
    configurable:true,
    enumerable:true
  })

看运行结果:
在这里插入图片描述
看到了吗?在将configurable修改为false前后都可以修改属性a的值,但是你想再用Object.defineProperty()修改属性描述符就会报错TypeError。所以将configurable修改为false是单向操作,不可再更改。
注意: 当configurable:false时,我们还是可以将writable的状态由true修改为false,但无法由false改为true。

1.6.3 enumerable

该描述符控制着属性是否会出现在对象的属性枚举中。比如说,在for…in循环,当enumerable:false时,这个属性就不会出现在枚举中,即循环遍历不到它,但还是可以正常用.操作符访问它。

1.7 不变性

有时候我们希望属性或者对象是不可改变的。在ES5是提供了很多方法来实现,但所有方法都是浅不变性。即它们只会影响目标对象的和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数等) ,其它对象的内容不受影响,仍然是可变的。
举例使用代码:

myObj={
	a:[1,2,3]
}
myObj.a.push(4);
myObj.a;//[1,2,3,4]

假设上面myObj已经是被设置为不可变的,但由于属性a是数组,所以它仍然可变。
想要实现深不变性还需添加下面方法使myObj.a也不变。

1.7.1 常量对象

结合writable:false和configurable:false就可以创建一个真正的常量属性(不可修改、重定义、删除)

var myObj={};
Object.defineProperty(obj,"属性子对象",{
	writable:false,
	configurable:false
	})
1.7.2 禁止扩展

如果你想禁止一个对象添加新属性并保留自己的属性,可以使用下面方法。

Object.preventExtensions(obj)

看下面代码:

var myObj={
	a:2
	};
Object.preventExtensions(obj);
Object.defineProperty(obj,"b",{});//尝试再添加属性b

在这里插入图片描述
结果是报错,不可再扩展。

1.7.3 密封

Object.seal()会创建一个“密封”对象,这个方法实际会在一个现有对象上调用Object.preventExtensions()并把所有现有属性设置为configurable:false;密封之后不能再添加新对象,也不能再重新配置或者删除任何现有属性(还是可以改变属性值)

1.7.4 冻结(最高级别)

Object.freeze()会创建一个冻结对象,这个方法实际上是在一个现有对象上调用Object.seal()并把所有属性标记为writable:false。这样就无法修改属性值。(不过就像之前所说的,这个对象引用的其他对象是不受影响的)

1.8 存在性

看下面代码:

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

两者都返回undefined,但第一个是属性a的真实值,第二个属性是未定义的属性。
由于仅根据返回值无法判断出是变量的值为undefined还是变量不存在,那我们怎么区分的?

看下面代码:

var obj={
  a:"aaa"
}
function check(obj){
  console.log("a" in obj);
  console.log("b" in obj);
  console.log(obj.hasOwnProperty("a"));
  console.log(obj.hasOwnProperty("b"));
}

在这里插入图片描述
in操作符会检查属性是否在对象及其[prototype]原型链中。而hasOwnProperty()只会检查obj对象中的属性,不会检查[prototype]链

检查是否存在某属性也可以使用“枚举”

var obj={
  a:"aaa"
}
function check(obj){
  Object.defineProperty(obj,"b",{
    value:5,
    enumerable:true//可枚举
  })
  Object.defineProperty(obj,"c",{
    value:6,
    enumerable:false//不可枚举
  })
  for(var key in obj){
    console.log(key);
  }
}

在这里插入图片描述
看到了吧?属性c不可被枚举,for…in遍历不到它,但在所有属性都是可枚举时,我们可以使用for…in来检查某个属性是否存在,从而判断出undefined是变量值还是为定义的变量。

1.9 遍历

由上面代码只是遍历出属性名,那怎么遍历属性值呢?

1.9.1 尝试使用.操作符
var obj={
  a:"aaa"
}
function check(obj){
  Object.defineProperty(obj,"b",{
    value:5,
    enumerable:true
  })
  Object.defineProperty(obj,"c",{
    value:6,
    enumerable:false//不可枚举
  })
  for(var key in obj){
    console.log(obj.key);
  }
}

在这里插入图片描述
看运行结果可知道用.操作符是无法读取到属性值的。

1.9.2 [ ]代替.操作符
var obj={
  a:"aaa"
}
function check(obj){
  Object.defineProperty(obj,"b",{
    value:5,
    enumerable:true
  })
  Object.defineProperty(obj,"c",{
    value:6,
    enumerable:false//不可枚举
  })
  for(var key in obj){
    console.log(obj[key]);
  }
}

在这里插入图片描述
这就遍历出属性值了。

1.9.3 ES6提供了for…of循环语法

该语法用在数组上:

var array=[1,2,3,5,4]
function check(array){
  for(var key of array){
    console.log(key);
  }
}

在这里插入图片描述
那这么好的方法能用在对象上吗?

var obj={
  a:"aaa",
  b:"bbb",
  c:"ccc"
}
function check(obj){
  
  for(var key of obj){
    console.log(key);
  }
}

在这里插入图片描述
直接用在对象身上是会报错的,对象不是一个迭代器。

看看for…of原理:for…of循环首先会向被访问对象请求一个迭代对象,然后通过迭代对象的next()方法来遍历所有返回的值。数组之所以能直接使用,是因为数组由内置的@@iterator。ES6使用Symbol.iterator来获取对象的@@iterator内部属性。@@iterator本身不是一个迭代器对象,而是一个返回迭代器对象的函数。

普通对象没有内置的@@iterator,所以无法自动完成遍历。

当然,我们可以给任何想遍历的对象定义@@iterator,看代码:

var obj={
  a:"aaa",
  b:"bbb",
  c:"ccc"
}
function check(obj){
  Object.defineProperty(obj,Symbol.iterator,{//添加迭代对象
    enumerable:false,//在遍历时该迭代属性不可读取
    writable:false,
    configurable:true,
    value:function(){
      var that=this;//指向obj
      var index=0;
      var k=Object.keys(that);//返回一个数组,记录着对象的属性名
      return{
        next:function(){
         return { 
           value:that[k[index++]],
           done:(index>k.length)//不可忽视
          }
        }
      }
    }
  })

  for(var key of obj){
    console.log(key);
  }
}

在这里插入图片描述
你看,给对象添加一个迭代对象就可以使用for…of循环来遍历对象的属性值了。

分析:

1.enumerable:false,//在遍历时该迭代属性不可读取
2.Object.keys(obj) 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该
对象时返回的顺序一致 。
参数必须是一个对象,否则报错typeError。
3.next()方法返回一个对象,value是当前遍历的属性值,done是一个布尔类型的值,表示是否继续遍历,控制着循环的
结束。

好啦,到这里对象就几乎学完了,这些有对你认识的对象有全新的认识吗?如有错误多多指教

猜你喜欢

转载自blog.csdn.net/weixin_43334673/article/details/106586363
今日推荐