es-object

基本

对象是 JS 中的引用数据类型

对象是一种复合数据类型,在对象中可以保存多个不同数据类型的属性

使用 typeof 检查一个对象时,会返回 'object',数据类型 Object 是大写的,但是返回的是小写

对象的分类

内建对象

由 ES 标准中定义的对象,在任何的 ES 实现中都可以直接使用

比如:Math. String() Number() Boolean()

宿主对象

由 JS 的运行环境提供的对象,主要是指由浏览器提供的对象

比如: BOM DOM console.log document.write

自定义对象

由开发人员自己创建的对象

创建对象

Object 构造函数

  var obj = new Object()  ;

运用 new 关键词调用函数 Object() 创建对象

这里 Object() 被称为构造函数 constructor,构造函数是专门用来创建对象的

对象字面量

名值对

var obj = {
  属性名:属性值,
  属性名:属性值,
	属性名:属性值,
	属性名(变量名):{名值对} //对象的值可以是对象
	方法名:function(){    }
}; //赋值语句,分号结尾

属性名可以加引号,因为属性名的本质都是字符串

但是,使用特殊属性名时一定要加,如 'class' 等关键字或者 123

注意,使用字面量形式时,属性值也可以不直接声明 @@@

var obj = {a}
// obj.a = undefined ,并不会报错

对象的基本操作

向对象中添加属性

语法:

对象.属性名 = 属性值;
对象[“属性名”] = 属性值;

对象的属性名没有任何要求,不需要遵守标识符的规范,但是还是应该尽量去遵守

用字面量创建更加灵活,能以数字为属性名,能传递变量调用相应的属性

obj[“123”] = “一二三”;
var n = “123”;
console.log(obj[n]); //注意n没有双引号,因为"123"

属性值可以任意数据类型

obj.name = "庞劲"; //typeof string
obj.age = 18; // typeof number
obj.b = true;// typeof boolean
obj.obj2

读取对象中的属性

语法:

对象.属性名
对象["属性名"] //有引号

如果读取一个对象中没有的属性,它不会报错,而是返回一个 undefined

修改对象的属性值

语法:

对象.属性名=新值;
对象["属性名"] = 新值;

删除对象的属性

语法:

delete 对象.属性名;
delete 对象["属性名"];

In 运算符

语法:" 属性名 " in 对象

如果在对象或其原型链中含有该属性,则返回 true

如果没有则返回 false

不建议使用 in,性能不好,因为原型链往往很长

Instanceof

instanceof 运算符用于测试 构造函数 的 prototype 属性是否出现在对象的原型链中的任何位置

判断对象的是否是某个 具体类(构造函数的实例)

返回值

示例

    语法:对象 instanceof 构造函数
    console.log(per instanceof Person); //true

注意

const str = new String('123')

console.log('123' instanceof String) // false
console.log('123' instanceof Object) // false
console.log(str instanceof String) // true
console.log(Object.prototype.toString.call('123')) // string
console.log(Object.prototype.toString.call(str)) // string

es-proto

hasOwnProperty()

这个方法可以用来检查对象自身中是否含有某个属性,

语法:对象.hasOwnProperty("属性名")

因为 in 无法区分自身的属性和原型属性

hasOwnProperty() 是在原型对象的原型的方法,即 Object 对象的方法

变量基本数据类型和引用类型

基本数据类型的数据,变量是直接保存的它的值

变量与变量之间是互相独立的,修改一个变量不会影响其他的变量。

基本数据类型:

引用数据类型,变量保存的对象的引用(堆内存地址、通过指针)

  obj2 = obj //并没有构建函数,而是把obj的指针复制给obj2

比较两个变量时

var c=10,d=10 ; // c==d true
var obj3 = new Object();
var obj4 = new Object(); //两者属性、方法完全一致, obj3 == obj4 false 像是两个双胞胎
变量
a 123
b (b = a) 123 (把 a 的值复制下来)
obj 0x123 (堆内存的地址,保存着 obj 的属性和方法)
obj2 (obj2 = obj) 0x123(把 obj 的值复制下来,有了相同的指针)

扩展

属性的简洁表示法

ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。下面是另一个例子。

function f(x, y) {
  return {x, y};
}

// 等同于

function f(x, y) {
  return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}

除了属性简写,方法也可以简写。

const o = {
  method() {
    return "Hello!";
  }
};

// 等同于

const o = {
  method: function() {
    return "Hello!";
  }
};

属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。

const cart = {
  _wheels: 4,

  get wheels () {
    return this._wheels;
  },

  set wheels (value) {
    if (value < this._wheels) {
      throw new Error('数值太小了!');
    }
    this._wheels = value;
  }
}

注意,简洁写法的属性名总是字符串,这会导致一些看上去比较奇怪的结果。

const obj = {
  class () {}
};

// 等同于

var obj = {
  'class': function() {}
};

上面代码中,class 是字符串,所以不会因为它属于关键字,而导致语法解析报错。

如果某个方法的值是一个 Generator 函数,前面需要加上星号。

const obj = {
  * m() {
    yield 'hello world';
  }
};

属性名表达式

JavaScript 定义对象的属性,有两种方法。

// 方法一
obj.foo = true;

// 方法二
obj['a' + 'bc'] = 123;

上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。

但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。

var obj = {
  foo: true,
  abc: 123
};

ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

表达式还可以用于定义方法名。

let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

obj.hello() // hi

注意,属性名表达式与简洁表示法,不能同时使用,会报错。

// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};

注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串 [object Object],这一点要特别小心。

const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
  [keyA]: 'valueA',
  [keyB]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}

上面代码中,[keyA][keyB] 得到的都是 [object Object],所以 [keyB] 会把 [keyA] 覆盖掉,而 myObject 最后只有一个 [object Object] 属性。

方法的 Name 属性

函数的 name 属性,返回函数名。对象方法也是函数,因此也有 name 属性。

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"

上面代码中,方法的 name 属性返回函数名(即方法名)。

如果对象的方法使用了取值函数(getter)和存值函数(setter),则 name 属性不是在该方法上面,而是该方法的属性的描述对象的 getset 属性上面,返回值是方法名前加上 getset

const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

如果对象的方法是一个 Symbol 值,那么 name 属性返回的是这个 Symbol 值的描述。

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""

上面代码中,key1 对应的 Symbol 值有描述,key2 没有。

Super 关键字

我们知道,this 关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字 super,指向当前对象的原型对象。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

上面代码中,对象 obj.find() 方法之中,通过 super.foo 引用了原型对象 protofoo 属性。

注意,super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

  // 报错
  const obj = {
    foo: super.foo
  }
  
  // 报错
  const obj = {
    foo: () => super.foo
  }
  
  // 报错
  const obj = {
    foo: function () {
      return super.foo
    }
  }

上面三种 super 的用法都会报错,因为对于 JavaScript 引擎来说,这里的 super 都没有用在对象的方法之中。第一种写法是 super 用在属性里面,第二种和第三种写法是 super 用在一个函数里面,然后赋值给 foo 属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。 @@@

JavaScript 引擎内部,super.foo 等同于 Object.getPrototypeOf(this).foo(属性)或 Object.getPrototypeOf(this).foo.call(this)(方法)。

  const proto = {
    x: 'hello',
    foo() {
      console.log(this.x);
    },
  };
  
  const obj = {
    x: 'world',
    foo() {
      super.foo();
    }
  }
  
  Object.setPrototypeOf(obj, proto);
  
  obj.foo() // "world"

上面代码中,super.foo 指向原型对象 protofoo 方法,但是绑定的 this 却还是当前对象 obj,因此输出的就是 world

对象的扩展运算符

es-next-1

静态方法

ES5 比较两个值是否相等,只有两个运算符:相等运算符(\==)和严格相等运算符(\===)。它们都有缺点,前者会自动转换数据类型,后者的 NaN 不等于自身,以及 +0 等于 -0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is 就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(\===)的行为基本一致。

不同之处只有两个:一是 +0 不等于 -0,二是 NaN 等于自身。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(+0,0) // true
Object.is(NaN, NaN) // true

Object.assign()

基本用法

Object.assign 方法用于对象的合并,将源对象 自身(source)的 所有可枚举属性,复制到目标对象(target)。

  const target = { a: 1 };
  
  const source1 = { b: 2 };
  const source2 = { c: 3 };
  
  Object.assign(target, source1, source2);
  target // {a:1, b:2, c:3}

Object.assign 方法的第一个参数是目标对象,后面的参数都是源对象。

属性名为 Symbol 值的属性,也会被 Object.assign 拷贝。

Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }

参数

Target

如果只有一个参数,Object.assign 会直接返回该参数。

  const obj = {a: 1};
  Object.assign(obj) === obj // true

如果该参数不是对象,则会先转成对象,然后返回。

  typeof Object.assign(2) // "object"

由于 undefinednull 无法转成对象,所以如果它们作为参数,就会报错。

Object.assign(undefined) // 报错
Object.assign(null) // 报错

Sources

如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果 undefinednull 不在首参数,就不会报错。

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true

其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。

  const v1 = 'abc';
  const v2 = true;
  const v3 = 10;
  
  const obj = Object.assign({}, v1, v2, v3);
  console.log(obj); // { "0": "a", "1": "b", "2": "c" }
  //上面代码中,v1、v2、v3分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。

布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性 [[PrimitiveValue]] 上面,这个属性是不会被 Object.assign 拷贝的。只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝。

  Object(10)  //  {[[PrimitiveValue]]: 10}
  Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}

注意

target 本身也会被修改,同时也会返回修改后的 target 的引用

如果目标对象与源对象有 同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性

浅拷贝

Object.assign 方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

  const obj1 = {a: {b: 1}};
  const obj2 = Object.assign({}, obj1);
  
  obj1.a.b = 2;
  obj2.a.b // 2

上面代码中,源对象 obj1a 属性的值是一个对象,Object.assign 拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

数组的处理

Object.assign 可以用来处理数组,但是会把数组视为对象。

  Object.assign([1, 2, 3], [4, 5])
  // [4, 5, 3]

上面代码中,Object.assign 把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性 4 覆盖了目标数组的 0 号属性 1

取值函数的处理

Object.assign 只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

const source = {
  get foo() { return 1 }
};
const target = {};

Object.assign(target, source)
// { foo: 1 }

上面代码中,source 对象的 foo 属性是一个 取值函数Object.assign 不会复制这个取值函数,只会拿到值以后,将这个值复制过去。

常见用途

为对象添加属性

通过 Object.assign 方法,将 x 属性和 y 属性添加到 Point 类的对象实例。

  class Point {
    constructor(x, y) {
      Object.assign(this, {x, y});
    }
  }

为对象添加方法

下面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用 assign 方法添加到 SomeClass.prototype 之中。

  Object.assign(SomeClass.prototype, {
    someMethod(arg1, arg2) {
      ···
    },
    anotherMethod() {
      ···
    }
  });
  
  // 等同于下面的写法
  SomeClass.prototype.someMethod = function (arg1, arg2) {
    ···
  };
  SomeClass.prototype.anotherMethod = function () {
    ···
  };

克隆对象

  function clone(origin) {
    return Object.assign({}, origin);
  }

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

  function clone(origin) {
    let originProto = Object.getPrototypeOf(origin);
    return Object.assign(Object.create(originProto), origin);
  }

合并多个对象

将多个对象合并到某个对象。

  const merge =
    (target, ...sources) => Object.assign(target, ...sources);

如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

  const merge =
    (...sources) => Object.assign({}, ...sources);

为属性指定默认值

  const DEFAULTS = {
    logLevel: 0,
    outputFormat: 'html'
  };
  
  function processContent(options) {
    options = Object.assign({}, DEFAULTS, options);
    console.log(options);
    // ...
  }

上面代码中,DEFAULTS 对象是默认值,options 对象是用户提供的参数。Object.assign 方法将 DEFAULTSoptions 合并成一个新对象,如果两者有同名属性,则 option 的属性值会覆盖 DEFAULTS 的属性值。

注意,由于存在浅拷贝的问题,DEFAULTS 对象和 options 对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS 对象的该属性很可能不起作用。

const DEFAULTS = {
  url: {
    host: 'example.com',
    port: 7070
  },
};

processContent({ url: {port: 8000} })
// {
//   url: {port: 8000}
// }

上面代码的原意是将 url.port 改成 8000,url.host 不变。实际结果却是 options.url 覆盖掉 DEFAULTS.url,所以 url.host 就不存在了。

Object.freeze()

如果真的想将对象冻结,应该使用 Object.freeze 方法。

  const foo = Object.freeze({});

  // 常规模式时,下面一行不起作用;
  // 严格模式时,该行会报错
  foo.prop = 123;

上面代码中,常量 foo 指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

属性描述符相关方法

基本概念

ECMAScript 对象中目前存在的 属性描述符 主要有两种, 数据描述符 (数据属性) 和存取描述符 (访问器属性):

数据 (数据描述符) 属性

数据属性有 4 个描述内部属性的特性

[[Configurable]]

表示能否通过 delete 删除此属性,能否修改属性的特性,或能否修改把属性 修改为访问器属性

默认值为 true 的 case

不使用 var 声明的全局变量,逐步淘汰

默认值为 false 的 case

使用 var 声明的全局变量,逐步淘汰

delete 只是不可删除,并不会报错,在严格模式下抛出错误

[[Enumerable]]

表示该属性是否可枚举,即是否通过for-in 循环或 Object.keys() 返回属性

如果直接使用 字面量定义对象,默认值为 true

[[Writable]]

能否 修改 属性的值

如果直接使用字面量定义对象,默认值为 true

[[Value]]

该属性对应的值,默认为 undefined

访问器 (存取描述符) 属性

访问器属性也有 4 个描述内部属性的特性

[[Configurable]]

和数据属性的 [[Configurable]] 一样,表示能否通过 delete 删除此属性,能否修改属性的特性,或能否修改把属性修改为 数据属性

如果直接使用字面量定义对象,默认值为 true

[[Enumerable]]

和数据属性的 [[Configurable]] 一样,表示该属性是否可枚举,即是否通过 for-in 循环或 Object.keys() 返回属性

如果直接使用字面量定义对象,默认值为 true

[[Get]]

一个给属性提供 getter 的方法 (访问对象属性时调用的函数,返回值就是当前属性的值),如果没有 getter 则为 undefined。该方法的 返回值被用作属性值

默认为 undefined

[[Set]]

一个给属性提供 setter 的方法 (给对象属性设置值时调用的函数),如果没有 setter 则为 undefined。该方法将默认接受 唯一参数:属性的新值

默认为 undefined,如果不在 setter 内部给 属性赋值,属性的值不会改变

对象内的 Get 和 Set 方法

可以在创建对象字面量的时候直接使用 get 和 set 方法:

  1. get 与 set 是 操作符
  2. get 是得到 一般是要返回的 set 是设置 不用返回
  3. 如果调用对象内部的属性 约定的命名方式 是带上下划线代表私有变量 _bar
var myObject = {
  foo: 4,
  _bar:1,
  get bar(){
    return this._bar
  },
  set bar(value) {
    this.foo = value
  }
}
console.log(myObject.bar=1,myObject.foo) // 1,1
// 上面的代码中通过get和set操作符定义了bar属性的setter
// 如果只有set,则bar属性不可读,因为get默认是undefined,相当于创建了一个只写属性,bar没有真正的值
// 如果只有get,则bar属性不可写,因为set默认是undefined,相当于创建了一个只读属性,bar不可写

在访问器属性描述符中的 [[get]][[set]] 都是描述符,本质上是调用对象自身的 get 和 set 操作符

字面量和属性描述在 Getter 和 Setter 上的区别

  1. 访问对象的属性是否需要加 this

  2. getter 和 setter 简写的形式不同

  3. 共同点:所有对属性的读写本质上都是通过 get 和 set

    var myObject = {
      foo:1,
      _bar:1,
      get bar(){
        return this._bar // 必须要加this,否则无法访问到外部属性
      },
      set bar(value) {
        this.foo = value
        console.log('set:bar')
      } 
    }
    Object.defineProperty(myObject, 'foo', {
      configurable : true,
      enumerable : true,
      get: function() {
          console.log('get:'+this)
          return foo // foo 可以直接访问,加了this反而报错,加了this 变成了访问foo,又会触发getter
      },
      set: function(newValue) {
          foo = newValue
          console.log('set:'+this)
      }
    })
    myObject.bar = 4
    console.log(myObject.bar,myObject.foo) //1,4
    myObject.foo = 2
    console.log(myObject.bar,myObject.foo) //1,2
    

ES5.1 扩展

Object.defineProperty(obj, Prop, descriptor)

该方法会直接在一个对象上定义 一个新属性,或者 修改一个对象的现有属性, 并 返回这个对象

如果不指定 configurable, writable, enumerable ,则这些属性默认值为 false,如果不指定 value, get, set,则这些属性默认值为 undefined

参数

注意

Object.defineProperties(object,descriptors)

方法直接在一个对象上定义 一个或多个新的属性或修改现有属性,并返回该对象

参数

  var obj = new Object();
  Object.defineProperties(obj, {
      name: {
          value: '张三',
          configurable: false,
          writable: true,
          enumerable: true
      },
      age: {
          value: 18,
          configurable: true
      }
  })
  
  console.log(obj.name, obj.age) // 张三, 18

Object.getOwnPropertyDescriptor(obj, prop)

返回指定对象上一个 自有属性对应的属性描述符

参数

  var person = {
      name: '张三',
      age: 18
  }
  
  var desc = Object.getOwnPropertyDescriptor(person, 'name'); 
  console.log(desc)  结果如下
  // {
  //     configurable: true,
  //     enumerable: true,
  //     writable: true,
  //     value: "张三"
  // }

ES6 扩展

Object.getOwnPropertyDescriptors(obj, prop)

ES2017 引入了 Object.getOwnPropertyDescriptors() 方法,返回指定对象所有 自身属性(非继承属性)的 描述对象

  const obj = {
    foo: 123,
    get bar() { return 'abc' }
  };
  
  Object.getOwnPropertyDescriptors(obj)
  // { foo:
  //    { value: 123,
  //      writable: true,
  //      enumerable: true,
  //      configurable: true },
  //   bar:
  //    { get: [Function: get bar],
  //      set: undefined,
  //      enumerable: true,
  //      configurable: true } }

上面代码中,Object.getOwnPropertyDescriptors() 方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。

polyfill

  function getOwnPropertyDescriptors(obj) {
    const result = {};
    for (let key of Reflect.ownKeys(obj)) {
      result[key] = Object.getOwnPropertyDescriptor(obj, key);
    }
    return result;
  }

拷贝 Get 和 Set

该方法的引入目的,主要是为了解决 Object.assign() 无法正确拷贝 get 属性和 set 属性的问题。

  const source = {
    set foo(value) {
      console.log(value);
    }
  };
  
  const target1 = {};
  Object.assign(target1, source);
  
  Object.getOwnPropertyDescriptor(target1, 'foo')
  // { value: undefined,
  //   writable: true,
  //   enumerable: true,
  //   configurable: true }

上面代码中,source 对象的 foo 属性的值是一个赋值函数,Object.assign 方法将这个属性拷贝给 target1 对象,结果该属性的值变成了 undefined。这是因为 Object.assign 方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。

这时,Object.getOwnPropertyDescriptors() 方法配合 Object.defineProperties() 方法,就可以实现正确拷贝。

const source = {
    set foo(value) {
        console.log(value);
    }
};

const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo')
// { get: undefined,
//   set: [Function: set foo],
//   enumerable: true,
//   configurable: true }

// 合并两个函数
const shallowMerge = (target, source) => Object.defineProperties(
    target,
    Object.getOwnPropertyDescriptors(source)
);

继承

另外,Object.getOwnPropertyDescriptors() 方法可以实现一个对象继承另一个对象。以前,继承另一个对象,常常写成下面这样。

const obj = {
  __proto__: prot,
  foo: 123,
};

ES6 规定 __proto__ 只有浏览器要部署,其他环境不用部署。如果去除 __proto__,上面代码就要改成下面这样。

const obj = Object.create(prot);
obj.foo = 123;

// 或者

const obj = Object.assign(
  Object.create(prot),
  {
    foo: 123,
  }
);

有了 Object.getOwnPropertyDescriptors(),我们就有了另一种写法。

const obj = Object.create(
  prot,
  Object.getOwnPropertyDescriptors({
    foo: 123,
  })
);

混入模式

Object.getOwnPropertyDescriptors() 也可以用来实现 Mixin(混入)模式。

  let mix = (object) => ({
    with: (...mixins) => mixins.reduce(
      (c, mixin) => Object.create(
        c, Object.getOwnPropertyDescriptors(mixin)
      ), object)
  });
  
  // multiple mixins example
  let a = {a: 'a'};
  let b = {b: 'b'};
  let c = {c: 'c'};
  let d = mix(c).with(a, b);
  
  d.c // "c"
  d.b // "b"
  d.a // "a"

上面代码返回一个新的对象 d,代表了对象 ab 被混入了对象 c 的操作。

出于完整性的考虑,Object.getOwnPropertyDescriptors() 进入标准以后,以后还会新增 Reflect.getOwnPropertyDescriptors() 方法。

各种场景下描述符属性的的扩展示例讲解

在对象中添加数据描述符属性

如果设置 configurable 属性为 false,则不可使用 delete 操作符 (在严格模式下抛出错误), 修改所有内部属性值会抛出错误

在《javaScript 高级教程中》说只可以改变 writable 的值,现在改变 writable 的值也会抛出错误

  var person = {};
  
  Object.defineProperty(person, 'name', {
      configurable: false,
      value: 'John'
  }) ;
  
  delete person.name   // 严格模式下抛出错误
  
  console.log(person.name)  // 'John'  没有删除
  
  Object.defineProperty(person, 'name', {
      configurable: true  //报错
  });
  
  Object.defineProperty(person, 'name', {
      enumerable: 2  //报错
  });
  
  Object.defineProperty(person, 'name', {
      writable: true  //报错
  });
  
  Object.defineProperty(person, 'name', {
      value: 2  //报错
  });

注意:

以上是最开始定义属性描述符时,writable 默认为 false,才会出现上述效果,如果 writable 定义为 true, 则可以修改 [[writable]][[value]] 属性值,修改另外两个属性值 仍然会 报错

  var obj = {};
  
  Object.defineProperty(obj, 'a', {
      configurable: false,
      writable: true,
      value: 1
  });
  
  Object.defineProperty(obj, 'a', {
      // configurable: true, //报错
      // enumerable: true,  //报错
      writable: false,
      value: 2
  });
  var d = Object.getOwnPropertyDescriptor(obj, 'a')
  console.log(d);
  // {
  //     value: 2, 
  //     writable: false, 
  // }

在对象中添加存取描述符属性

  var obj = {};
  var aValue; //如果不初始化变量, 不给下面的a属性设置值,直接读取会报错aValue is not defined
  var b;
  Object.defineProperty(obj, 'a', {
      configurable : true,
      enumerable : true,
      get: function() {
          return aValue
      },
      set: function(newValue) {
          aValue = newValue;
          b = newValue + 1
      }
  })
  console.log(b) // undefined
  console.log(obj.a)  // undefined, 当读取属性值时,调用get方法,返回undefined
  obj.a = 2;  // 当设置属性值时,调用set方法,aValue为2
  
  console.log(obj.a) // 2  读取属性值,调用get方法,此时aValue为2
  console.log(b) // 3  再给obj.a赋值时,执行set方法,b的值被修改为2,额外说一句,vue中的计算属性就是利用setter来实现的

注意:

  1. getter 和 setter 可以不同时使用,但在严格模式下只其中一个,会抛出错误
  2. 数据描述符与存取描述符不可混用,会抛出错误
  var obj = {};
  Object.defineProperty(obj, 'a', {
      value: 'a1',
      get: function() {
         return 'a2'
      }    
  });
  // TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute

[[Writable]]

当 writable 为 false(并且 configurable 为 true),[[value]] 可以通过 defineProperty 修改, 但 不能直接赋值修改

  var obj = {};
  
  Object.defineProperty(obj, 'a', {
      configurable: true,
      enumerable: false,
      writable: false,
      value: 1
  });
  
  Object.defineProperty(obj, 'a', {
      configurable: false,
      enumerable: true,
      writable: false ,
      value: 2
  });
  var d = Object.getOwnPropertyDescriptor(obj, 'a')
  
  console.log(d); // 结果如下
  // {
  //     value: 2, 
  //     writable: false, 
  //     enumerable: true, 
  //     configurable: false
  // }
  
  
  // 但是如果直接赋值修改
  var obj = {}
  
  Object.defineProperty(obj, 'a', {
      configurable: true,
      enumerable: false,
      writable: false,
      value: 1
  });
  obj.a=2;
  var d = Object.getOwnPropertyDescriptor(obj, 'a')
  
  console.log(d); // 结果如下
  
  // {
  //     value: 1,  // 没有做出修改,不会报错
  //     writable: false, 
  //     enumerable: true, 
  //     configurable: false
  // }

[[Enumerable]]

  var obj = {};
  Object.defineProperties(obj, {
      a: {
          value: 1,
          enumerable: false
      }, 
      b: {
          value: 2,
          enumerable: true
      },
      c: {
          value: 3,
          enumerable: false
      }
  })
  
  obj.d = 4;
  
  //等同于
  
  //Object.defineProperty(obj, 'd', {
  //    configurable: true,
  //    enumerable: true,
  //    writable: true,
  //    value: 4
  //})
  
  for(var key in obj) {
      console.log(key);  
      // 打印一次b, 一次d, a和c属性enumerable为false,不可被枚举
  } 
  
  var arr = Object.keys(obj);
  console.log(arr);  // ['b', 'd']

Get 和 Set 实现简单的数据双向绑定

  <body>
      <p>
          input1=><input type="text" id="input1">
      </p>
      <p>
          input2=><input type="text" id="input2">
      </p>
      <div>
          我每次比input2的值加1=>
          <span id="span"></span>
      </div>
  </body>
  var oInput1 = document.getElementById('input1');
  var oInput2 = document.getElementById('input2');
  var oSpan = document.getElementById('span');
  var obj = {};
  Object.defineProperties(obj, {
      val1: {
          configurable: true,
          get: function() {
              oInput1.value = 0;
              oInput2.value = 0;
              oSpan.innerHTML = 0;
              return 0 //设置初始显示值
          },
          set: function(newValue) {
              oInput2.value = newValue;
              // 可以访问到val2的值吗?使用this可以读取到
              // this.val2 = newValue;
              oSpan.innerHTML = Number(newValue) ? Number(newValue)+1 : 0
          }
      },
      val2: {
          configurable: true,
          get: function() {
              oInput1.value = 0;
              oInput2.value = 0;
              oSpan.innerHTML = 0;
              return 0 //设置初始显示值
          },
          set: function(newValue) {
              oInput1.value = newValue;
              oSpan.innerHTML = Number(newValue)+1;
          }
      }
  })
  // 初始化显示,直接修改input1,input1的setter修改了input2和span
  oInput1.value = obj.val1;
  oInput1.addEventListener('keyup', function() {
      obj.val1 = oInput1.value;
  }, false)
  oInput2.addEventListener('keyup', function() {
      obj.val2 = oInput2.value;
  }, false)

遍历

可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor 方法可以获取该属性的描述对象。

  let obj = { foo: 123 };
  Object.getOwnPropertyDescriptor(obj, 'foo')
  //  {
  //    value: 123,
  //    writable: true,
  //    enumerable: true,
  //    configurable: true
  //  }

描述对象的 enumerable 属性,称为“可枚举性”,如果该属性为 false,就表示某些操作会忽略当前属性。

目前,有四个操作会忽略 enumerablefalse 的属性。

这四个操作之中,前三个是 ES5 就有的,最后一个 Object.assign() 是 ES6 新增的。其中,只有 for...in 会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。

实际上,引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉 for...in 操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的 toString 方法,以及数组的 length 属性,就通过“可枚举性”,从而避免被 for...in 遍历到。

  Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
  // false
  
  Object.getOwnPropertyDescriptor([], 'length').enumerable
  // false

上面代码中,toStringlength 属性的 enumerable 都是 false,因此 for...in 不会遍历到这两个继承自原型的属性。

另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。

  Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
  // false

总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用 for...in 循环,而用 Object.keys() 代替。

for…in 语句

for(var 变量 in 对象){}

for(var n in obj){
	console.log(n); //打印属性名
	console.log(obj.n); //错误,obj里并没有属性n
	console.log(obj[n]);//正确,向obj动态传递了属性名,可以打印属性值
}

for…in 语句,对象中有几个属性,循环体就会执行几次

每次执行时, 会把一个属性的名字赋值给声明的变量

属性的遍历

ES6 一共有 5 种方法可以遍历对象的 属性名

for...in

for...in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

Object.keys(obj)

Object.keys 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols 返回一个数组,包含对象自身的所有 Symbol 属性的键名。

Reflect.ownKeys(obj)

Reflect.ownKeys 返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

共同点

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则:

  Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
  // ['2', '10', 'b', 'a', Symbol()]

ES5.1 重要扩展

Object.keys()

Object.keys 方法,返回一个数组,成员是参数 对象自身 的(不含继承的)所有可遍历(enumerable)属性的 键名

  var obj = { foo: 'bar', baz: 42 };
  Object.keys(obj)
  // ["foo", "baz"]

参数

exception

ES6 重要扩展

ES2017 引入了跟 Object.keys 配套的 Object.valuesObject.entries,作为遍历一个对象的补充手段,供 for...of 循环使用。

  let {keys, values, entries} = Object;
  let obj = { a: 1, b: 2, c: 3 };
  
  for (let key of keys(obj)) {
    console.log(key); // 'a', 'b', 'c'
  }
  
  for (let value of values(obj)) {
    console.log(value); // 1, 2, 3
  }
  
  for (let [key, value] of entries(obj)) {
    console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
  }

Object.values()

Object.values 方法返回一个数组,成员是参数 对象自身 的(不含继承的)所有可遍历(enumerable)属性的 键值

  const obj = { foo: 'bar', baz: 42 };
  Object.values(obj)
  // ["bar", 42]

返回数组的成员顺序,与本章的《属性的遍历》部分介绍的排列规则一致。

  const obj = { 100: 'a', 2: 'b', 7: 'c' };
  Object.values(obj)
  // ["b", "c", "a"]
  // 上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b、c、a。

Object.values 只返回对象自身的可遍历属性。

  const obj = Object.create({}, {p: {value: 42}});
  Object.values(obj) // []

上面代码中,Object.create 方法的第二个参数添加的对象属性(属性 p),如果不显式声明,默认是不可遍历的,因为 p 的属性描述对象的 enumerable 默认是 falseObject.values 不会返回这个属性。只要把 enumerable 改成 trueObject.values 就会返回属性 p 的值。

  const obj = Object.create({}, {p:
    {
      value: 42,
      enumerable: true
    }
  });
  Object.values(obj) // [42]

Object.values 会过滤属性名为 Symbol 值的属性。

  Object.values({ [Symbol()]: 123, foo: 'abc' });
  // ['abc']

参数

Object.entries()

Object.entries() 方法返回一个数组,成员是参数 对象自身 的(不含继承的)所有可遍历(enumerable)属性的 键值对数组

  const obj = { foo: 'bar', baz: 42 };
  Object.entries(obj)
  // [ ["foo", "bar"], ["baz", 42] ]

除了返回值不一样,该方法的行为与 Object.values 基本一致。

如果原对象的属性名是一个 Symbol 值,该属性会被忽略。

Object.entries({ [Symbol()]: 123, foo: 'abc' });
// [ [ 'foo', 'abc' ] ]

上面代码中,原对象有两个属性,Object.entries 只输出属性名非 Symbol 值的属性。将来可能会有 Reflect.ownEntries() 方法,返回对象自身的所有属性。

Object.entries 的基本用途是 遍历对象的属性

let obj = { one: 1, two: 2 };
for (let [k, v] of Object.entries(obj)) {
  console.log(
    `${JSON.stringify(k)}: ${JSON.stringify(v)}`
  );
}
// "one": 1
// "two": 2

Object.entries 方法的另一个用处是,将对象转为真正的 Map 结构。

const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }

Polyfill

  // Generator函数的版本
  function* entries(obj) {
    for (let key of Object.keys(obj)) {
      yield [key, obj[key]];
    }
  }
  
  // 非Generator函数的版本
  function entries(obj) {
    let arr = [];
    for (let key of Object.keys(obj)) {
      arr.push([key, obj[key]]);
    }
    return arr;
  }

Object.fromEntries()

Object.fromEntries() 方法是 Object.entries() 的逆操作,用于将一个键值对数组转为对象。

  Object.fromEntries([
    ['foo', 'bar'],
    ['baz', 42]
  ])
  // { foo: "bar", baz: 42 }

该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。

  // 例一
  const entries = new Map([
    ['foo', 'bar'],
    ['baz', 42]
  ]);
  
  Object.fromEntries(entries)
  // { foo: "bar", baz: 42 }
  
  // 例二
  const map = new Map().set('foo', true).set('bar', false);
  Object.fromEntries(map)
  // { foo: true, bar: false }

该方法的一个用处是配合 URLSearchParams 对象,将查询字符串转为对象。

  Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
  // { foo: "bar", baz: "qux" }

克隆

Api 实现的数组浅拷贝

Slice() concat()

可以利用数组的一些方法比如:slice、concat 返回一个新数组的特性来实现拷贝。

var arr = ['old', 1, true, null, undefined];

var new_arr = arr.concat();
var new_arr = arr.slice();

扩展运算符

  var arr = ['old', 1, true, null, undefined];
  var new_arr = [...arr]

Array.from()

如果参数是一个真正的数组,Array.from 会返回一个一模一样的新数组,浅克隆

var arr = ['old', 1, true, null, undefined];
var new_arr = Array.from(arr)

对比

扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。

Array.from 方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有 length 属性。因此,任何有 length 属性的对象,都可以通过 Array.from 方法转为数组,而此时扩展运算符就无法转换。

Array.from({ length: 3 });
// [ undefined, undefined, undefined ]

Object.assign() 实现对象浅拷贝

Object.assign()

Json 实现深拷贝

var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]

var new_arr = JSON.parse( JSON.stringify(arr) );

console.log(new_arr);

有一个问题,不能拷贝函数:

var arr = [function(){console.log(a)}, {b: function(){console.log(b)}}]

var new_arr = JSON.parse(JSON.stringify(arr));

console.log(new_arr);

按理说也不能拷贝 getter 和 setter

浅拷贝的实现

const shallowCopy = function(obj) {
	// 只拷贝对象
	if (typeof obj !== 'object') return;
	// 根据obj的类型判断是新建一个数组还是对象
	const newObj = obj instanceof Array ? [] : {};
	// 遍历obj,并且判断是obj的属性才拷贝
	// for (var key in obj) {
	//   if (obj.hasOwnProperty(key)) newObj[key] = obj[key]
	// }
	for (const [key,value] of Object.entries(obj)) {
		newObj[key] = value
	}
	return newObj;
}

深拷贝的实现

那如何实现一个深拷贝呢?说起来也好简单,我们在拷贝的时候判断一下属性值的类型,如果是对象,我们递归调用深拷贝函数不就好了~

const deepCopy = function(obj) {
	if (typeof obj !== 'object') return
	if (typeof obj === null) return null
	
	const newObj = obj instanceof Array ? [] : {}
	for (const [key,value] of Object.entries(obj)) {
		newObj[key] = typeof value === 'object' ? deepCopy(value) : value
	}
	return newObj;
}

注意 null,可以通过第一层判断,但是 Object.entries() 无法接受 null 作为参数, 所以需要再增加一层判断

console.log(deepCopy(null))
// TypeError: Cannot convert undefined or null to object at Function.entries (<anonymous>)

如果使用 ES5 API 则会赋值为一个空对象

面试一般只要求到这种程度就足够了

structuredClone()

structuredClone() - Web API 接口参考 | MDN

结构化克隆算法是 由HTML5规范定义 的用于复制复杂 JavaScript 对象的算法。通过来自 WorkerspostMessage() 或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。

目前支持的类型有:

不支持:

结构化克隆所不能做到的

Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。

企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERROR 异常。

对象的某些特定参数也不会被保留

浏览器实现

用浏览器自身的 API 来实现深度拷贝,有 MessageChannel、history api 、Notification api 等

http://caibaojian.com/deep-copy.html

拷贝 Getter 和 Setter

上面的 deepCopy 的实现同样无法拷贝 getter 和 setter

structureClone 也同样无法拷贝 getter 和 setter

es-object

$.extend()

基本用法

jQuery 的 extend 是 jQuery 中应用非常多的一个函数,今天我们一边看 jQuery 的 extend 的特性,一边实现一个 extend!

先来看看 extend 的功能,合并两个或者更多的对象的内容到第一个对象中。

jQuery.extend( target [, object1 ] [, objectN ] )

第一个参数 target,表示要拓展的目标,我们就称它为目标对象吧。

后面的参数,都传入对象,内容都会复制到目标对象中,我们就称它们为待复制对象吧。

当两个对象出现相同字段的时候,后者会覆盖前者,不会进行深层次的覆盖

var obj1 = {
  a: 1,
  b: { b1: 1, b2: 2 }
};

var obj2 = {
  b: { b1: 3, b3: 4 },
  c: 3
};

var obj3 = {
  d: 4
}

console.log($.extend(obj1, obj2, obj3));

// {
//    a: 1,
//    b: { b1: 3, b3: 4 },
//    c: 3,
//    d: 4
// }

Extend 第一版

function extend(target, ...options) {
  const len = options.length;
  for (let i = 0; i < len; i++) {
    option = options[i]
    if (option != null) {
      for (let [key, value] of Object.entries(option)) {
        if (value !== undefined) {
          target[key] = value;
        }
      }
    }
    return target
  }
}

Extend 深拷贝

那如何进行深层次的复制呢?jQuery v1.1.4 加入了一个新的用法:

jQuery.extend( [deep], target, object1 [, objectN ] )

也就是说,函数的第一个参数可以传一个布尔值,如果为 true,我们就会进行深拷贝,false 依然当做浅拷贝,这个时候,target 就往后移动到第二个参数。

var obj1 = {
  a: 1,
  b: { b1: 1, b2: 2 }
};

var obj2 = {
  b: { b1: 3, b3: 4 },
  c: 3
};

var obj3 = {
  d: 4
}

console.log($.extend(true, obj1, obj2, obj3));

// {
//    a: 1,
//    b: { b1: 3, b2: 2, b3: 4 },
//    c: 3,
//    d: 4
// }

因为采用了深拷贝,会遍历到更深的层次进行添加和覆盖。

Extend 第二版

我们来实现深拷贝的功能,值得注意的是:根据 copy 的类型递归 extend。

function deepExtend(target,...options) {
  var len = options.length;
  // 循环遍历要复制的对象们
for (let i = 0; i < len; i++) {
    option = options[i];
    // 要求不能为空 避免 deepExtend(a,,b) 这种情况
    if (option != null) {
      for (let [key,value] of Object.entries(option)) {
        if (value && typeof value == 'object') {
          // 是对象,进行递归调用
          target[key] = deepExtend(target[key], value)
        }
        else if (value !== undefined){
          // 不是对象,直接赋值
          target[key] = value
        }
      }
    }
  }
  return target;
};

在实现上,核心的部分还是跟上篇实现的深浅拷贝函数一致,如果要复制的对象的属性值是一个对象,就递归调用 extend。

优化

Target 是函数

类型不一致

循环引用

实际上,我们还可能遇到一个循环引用的问题,举个例子:

var a = {name : null}
var b = {name : null}
a.name = b
b.name = a
console.log(deepExtend(a, b))

我们会得到一个可以无限展开的对象,类似于这样:

为了避免这个问题,我们需要判断要复制的对象属性是否等于 target,如果等于,我们就跳过:

// 对象可能存在循环引用,此时应该跳过
if (target === value) {
  continue;
}

使用 WeakMap 来记录已经被拷贝过的对象,如果再次遇到同样的对象,直接返回它的克隆即可,解决了递归方法的循环引用问题。

最终代码

function deepExtend(target,...options) {
  var len = options.length;
  // 如果target不是对象,我们是无法进行复制的,所以设为{}
  if (typeof target !== 'object') {
    target = {}
  }
  // 循环遍历要复制的对象们
  for (let i = 0; i < len; i++) {
    option = options[i];
    // 要求不能为空 避免 deepExtend(a,,b) 这种情况
    if (option != null) {
      for (let [key,value] of Object.entries(option)) {
        // 排除null和undefined
        if (value && typeof value == 'object') {
          // 对象可能存在循环引用,此时应该跳过
          if(target === value) continue

          // 如果值是数组,保证target也是数组
          if(Array.isArray(value)) target[key] = Array.isArray(target[key])?target[key]:[]

          // 进行递归调用
          target[key] = deepExtend(target[key], value)
        }else if (value !== undefined){
          // 不是对象,直接赋值
          target[key] = value
        }
      }
    }
  }
  return target;
};

思考题

如果觉得看明白了上面的代码,想想下面两个 demo 的结果:

var a = extend(true, [4, 5, 6, 7, 8, 9], [1, 2, 3]);
console.log(a) // ??? 

var obj1 = {
  value: {
    3: 1
  }
}
var obj2 = {
  value: [5, 6, 7],
}

var b = extend(true, obj1, obj2) // ??? {value:[5,6,7]}
var c = extend(true, obj2, obj1) // ??? {value:[5,6,7,1]}

类型判断

Typeof

在 ES6 前,JavaScript 共六种数据类型,分别是:

Undefined、Null、Boolean、Number、String、Object

typeof 对这些数据类型的值进行操作的时候,返回的结果却不是一一对应,分别是:

undefined、object、boolean、number、string、object

注意:返回的字符串都是小写的typeof NaN // number

可以判断: undefined/ 数值 / 字符串 / 布尔值 / function

不能区分: null 与 object object 与 array , 不能区分出 object 的类

typeof null //object
typeof array //object
typeof NaN // number

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签也成为了 0,typeof null 就错误的返回了 "object"。(reference

ECMAScript 提出了一个修复(通过 opt-in),但 被拒绝。这使得 typeof null === 'object' 将成为长期的错误

Instanceof

es-object

全等运算符

可以判断: undefined, null

因为这两个类型都只有一个值

falsy

Object.prototype.tostring.call(target)

返回固定字符串: [object Target 类型]

Object.prototype.toString.call(target).slice(8,-1),就可以返回准确的类型

ES5 开始,可以检查 Null 和 undefined。ES5 规范地址:https://es5.github.io/#x15.2.4.2

Object.prototype.toString 方法被调用的时候,下面的步骤会被执行:

  1. 如果 this 值是 undefined,就返回 [object Undefined]
  2. 如果 this 的值是 null,就返回 [object Null]
  3. 让 O 成为 ToObject(this) 的结果
  4. 让 class 成为 O 的内部属性 [[Class]] 的值
  5. 最后返回由 "[object " 和 class 和 "]" 三个部分组成的字符串

无法判断自定义类型

Object.prototype.toString 可以识别出至少 12 种数据类型

// 以下是12种:
var number = 1;          // [object Number]
var string = '123';      // [object String]
var boolean = true;      // [object Boolean]
var und = undefined;     // [object Undefined]
var nul = null;          // [object Null]
var obj = {a: 1}         // [object Object]
var array = [1, 2, 3];   // [object Array]
var date = new Date();   // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g;          // [object RegExp]
var func = function a(){}; // [object Function]
// ES6
const sym = Symbol() // [object Symbol]

function checkType() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(Object.prototype.toString.call(arguments[i]))
  }
}

checkType(number, string, boolean, und, nul, obj, array, date, error, reg, func)
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]
console.log(Object.prototype.toString.call(window)); // [object Window]
function a() {
    console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
a();

注意:

封装 Type 函数

写一个 type 函数能检测各种类型的值

// 第一版
var class2type = {};

// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error Null Undefined".split(" ").map(function(item, index) {
    class2type["[object " + item + "]"] = item.toLowerCase();
})

function type(obj) {
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[Object.prototype.toString.call(obj)] || "object" :
        typeof obj;
}

Isfunction

有了 type 函数后,我们可以对常用的判断直接封装,比如 isFunction:

function isFunction(obj) {
    return type(obj) === "function";
}

IsArray

var isArray = Array.isArray || function( obj ) {
    return type(obj) === "array";
}

原型和构造函数都有被修改的可能性

Isarraylike

isArrayLike,看名字可能会让我们觉得这是判断类数组对象的,其实不仅仅是这样,jQuery 实现的 isArrayLike,数组和类数组都会返回 true

function isArrayLike(obj) {

  // obj 必须有 length属性
  var length = !!obj && "length" in obj && obj.length;
  var typeRes = type(obj);

  // 排除掉函数和 Window 对象
  if (typeRes === "function" || isWindow(obj)) {
    return false;
  }

  return typeRes === "array" || length === 0 ||
    typeof length === "number" && length > 0 && (length - 1) in obj;
}

重点分析 return 这一行,使用了或语句,只要一个为 true,结果就返回 true。

所以如果 isArrayLike 返回 true,至少要满足三个条件之一:

  1. 是数组
  2. 长度为 0
  3. lengths 属性是大于 0 的数字类型,并且 obj[length - 1] 必须存在

第一个就不说了,看第二个,为什么长度为 0 就可以直接判断为 true 呢?

那我们写个对象:

var obj = {a: 1, b: 2, length: 0}

isArrayLike 函数就会返回 true,那这个合理吗?

回答合不合理之前,我们先看一个例子:

function a(){
    console.log(isArrayLike(arguments))
}
a();

如果我们去掉 length === 0 这个判断,就会打印 false,然而我们都知道 arguments 是一个类数组对象,这里是应该返回 true 的。

所以是不是为了放过空的 arguments 时也放过了一些存在争议的对象呢?

第三个条件:length 是数字,并且 length > 0 且最后一个元素存在。

为什么仅仅要求最后一个元素存在呢?

让我们先想下数组是不是可以这样写:

var arr = [,,3]

当我们写一个对应的类数组对象就是:

var arrLike = {
    2: 3,
    length: 3
}

也就是说当我们在数组中用逗号直接跳过的时候,我们认为该元素是不存在的,类数组对象中也就不用写这个元素,但是最后一个元素是一定要写的,要不然 length 的长度就不会是最后一个元素的 key 值加 1。比如数组可以这样写

var arr = [1,,];
console.log(arr.length) // 2

但是类数组对象就只能写成:

var arrLike = {
    0: 1,
    length: 1
}

所以符合条件的类数组对象是一定存在最后一个元素的!

这就是满足 isArrayLike 的三个条件,其实除了 jQuery 之外,很多库都有对 isArrayLike 的实现,比如 underscore:

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

var isArrayLike = function(collection) {
    var length = getLength(collection);
    return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

Isplainobject

plainObject 来自于 jQuery,可以翻译成纯粹的对象,所谓 " 纯粹的对象 ",就是该对象是通过 "{}" 或 "new Object" 创建的,该对象含有零个或者多个键值对。

之所以要判断是不是 plainObject,是为了跟其他的 JavaScript 对象如 null,数组,宿主对象(documents)等作区分,因为这些用 typeof 都会返回 object。

jQuery 提供了 isPlainObject 方法进行判断,先让我们看看使用的效果:

function Person(name) {
  this.name = name;
}

console.log($.isPlainObject({})) // true

console.log($.isPlainObject(new Object)) // true

console.log($.isPlainObject(Object.create(null))); // true

console.log($.isPlainObject(Object.assign({a: 1}, {b: 2}))); // true

console.log($.isPlainObject(new Person('yayu'))); // false

console.log($.isPlainObject(Object.create({}))); // false

由此我们可以看到,除了 {} 和 new Object 创建的之外,jQuery 认为一个 没有原型的对象 也是一个纯粹的对象。

isPlainObject 的实现也在变化,我们今天讲的是 3.0 版本下的 isPlainObject,我们直接看源码:

// 上节中写 type 函数时,用来存放 toString 映射结果的对象
var class2type = {};

// 相当于 Object.prototype.toString
var toString = class2type.toString;

// 相当于 Object.prototype.hasOwnProperty
var hasOwn = class2type.hasOwnProperty;

function isPlainObject(obj) {
  var proto, Ctor;

  // 排除掉明显不是obj的以及一些宿主对象如Window
  if (!obj || toString.call(obj) !== "[object Object]") {
    return false;
  }

  proto = Object.getPrototypeOf(obj);

  // 没有原型的对象是纯粹的,Object.create(null) 就在这里返回 true
  if (!proto) {
    return true;
  }

  /**
   * 以下判断通过 new Object 方式创建的对象
   * 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor
   * 如果是 Object 函数创建的对象,Ctor 在这里就等于 Object 构造函数
   */
  Ctor = hasOwn.call(proto, "constructor") && proto.constructor;

  // 在这里判断 Ctor 构造函数是不是 Object 构造函数,用于区分自定义构造函数和 Object 构造函数
  return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}

注意:我们判断 Ctor 构造函数是不是 Object 构造函数,用的是 hasOwn.toString.call(Ctor),这个方法可不是 Object.prototype.toString,不信我们在函数里加上下面这两句话:

console.log(hasOwn.toString.call(Ctor)); // function Object() { [native code] }
console.log(Object.prototype.toString.call(Ctor)); // [object Function]

发现返回的值并不一样,这是因为 hasOwn.toString 调用的其实是 Function.prototype.toString,毕竟 hasOwnProperty 可是一个函数!

而且 Function 对象覆盖了从 Object 继承来的 Object.prototype.toString 方法。函数的 toString 方法会返回一个表示函数源代码的字符串。具体来说,包括 function 关键字,形参列表,大括号,以及函数体中的内容。

因此:这里要判断的其实是两个函数是否一样

Isemptyobject

for...in 遍历

for (var i in obj) { // 如果不为空,则会执行到这一步,返回true
    return false
}
return true // 如果为空,返回false

其实所谓的 isEmptyObject 就是判断是否有属性,for 循环一旦执行,就说明有属性,有属性就会返回 false。

但是根据这个源码我们可以看出 isEmptyObject 实际上判断的并不仅仅是空对象。

console.log(isEmptyObject({})); // true
console.log(isEmptyObject([])); // true
console.log(isEmptyObject(null)); // true
console.log(isEmptyObject(undefined)); // true
console.log(isEmptyObject(1)); // true
console.log(isEmptyObject('')); // true
console.log(isEmptyObject(true)); // true

但是既然 jQuery 是这样写,可能是因为考虑到实际开发中 isEmptyObject 用来判断 {} 和 {a: 1} 是足够的吧。如果真的是只判断 {},完全可以结合上篇写的 type 函数筛选掉不适合的情况。

通过 JSON 自带的 stringify() 方法来判断

if (JSON.stringify(data) === '{}') {
    return true // 如果为空,返回false
}
return false // 如果不为空,返回true

依靠 Object.keys()

if (Object.keys(object).length === 0) {
    return true // 如果为空,返回false
}
return false // 如果不为空,则会执行到这一步,返回true

isWindow

Window 对象作为客户端 JavaScript 的全局对象,它有一个 window 属性指向自身,这点在 《JavaScript深入之变量对象》 中讲到过。我们可以利用这个特性判断是否是 Window 对象。

function isWindow( obj ) {
    return obj != null && obj === obj.window;
}

isElement

isElement 判断是不是 DOM 元素。

isElement = function(obj) {
    return !!(obj && obj.nodeType === 1);
};

等值判断

Object.is()

+0 和 -0

如果 a === b 的结果为 true, 那么 a 和 b 就是相等的吗?一般情况下,当然是这样的,但是有一个特殊的例子,就是 +0 和 -0。

JavaScript “处心积虑”的想抹平两者的差异:

// 表现1
console.log(+0 === -0); // true

// 表现2
(-0).toString() // '0'
(+0).toString() // '0'

// 表现3
-0 < +0 // false
+0 < -0 // false

即便如此,两者依然是不同的:

1 / +0 // Infinity
1 / -0 // -Infinity

1 / +0 === 1 / -0 // false

也许你会好奇为什么要有 +0 和 -0 呢?

这是因为 JavaScript 采用了 IEEE_754 浮点数表示法 (几乎所有现代编程语言所采用),这是一种二进制表示法,按照这个标准,最高位是符号位 (0 代表正,1 代表负),剩下的用于表示大小。而对于零这个边界值 ,1000(-0) 和 0000(0) 都是表示 0 ,这才有了正负零的区别。

也许你会好奇什么时候会产生 -0 呢?

Math.round(-0.1) // -0

那么我们又该如何在 === 结果为 true 的时候,区别 0 和 -0 得出正确的结果呢?我们可以这样做:

function eq(a, b){
    if (a === b) return a !== 0 || 1 / a === 1 / b;
    return false;
}

console.log(eq(0, 0)) // true
console.log(eq(0, -0)) // false

NaN

在本篇,我们认为 NaN 和 NaN 是相等的,那又该如何判断出 NaN 呢?

console.log(NaN === NaN); // false

利用 NaN 不等于自身的特性,我们可以区别出 NaN,那么这个 eq 函数又该怎么写呢?

function eq(a, b) {
    if (a !== a) return b !== b;
}

console.log(eq(NaN, NaN)); // true

DeepEqual

String 对象

现在我们开始写 deepEq 函数,一个要处理的重大难题就是 'Curly' 和 new String('Curly') 如何判断成相等?

两者的类型都不一样呐!不信我们看 typeof 的操作结果:

console.log(typeof 'Curly'); // string
console.log(typeof new String('Curly')); // object

可是我们在 《JavaScript专题之类型判断上》 中还学习过更多的方法判断类型,比如 Object.prototype.toString:

var toString = Object.prototype.toString;
toString.call('Curly'); // "[object String]"
toString.call(new String('Curly')); // "[object String]"

神奇的是使用 toString 方法两者判断的结果却是一致的,可是就算知道了这一点,还是不知道如何判断字符串和字符串包装对象是相等的

那我们利用隐式类型转换呢?

console.log('Curly' + '' === new String('Curly') + ''); // true

看来我们已经有了思路:如果 a 和 b 的 Object.prototype.toString 的结果一致,并且都是 "[object String]",那我们就使用 '' + a === '' + b 进行判断。

可是不止有 String 对象呐,Boolean、Number、RegExp、Date 呢?

更多对象

跟 String 同样的思路,利用隐式类型转换。

Boolean

var a = true;
var b = new Boolean(true);

console.log(+a === +b) // true

Date

var a = new Date(2009, 9, 25);
var b = new Date(2009, 9, 25);

console.log(+a === +b) // true

RegExp

var a = /a/i;
var b = new RegExp(/a/i);

console.log('' + a === '' + b) // true

Number

var a = 1;
var b = new Number(1);

console.log(+a === +b) // true

嗯哼?你确定 Number 能这么简单的判断?

var a = Number(NaN);
var b = Number(NaN);

console.log(+a === +b); // false

可是 a 和 b 应该被判断成 true 的呐~

那么我们就改成这样:

var a = Number(NaN);
var b = Number(NaN);

function eq() {
    // 判断 Number(NaN) Object(NaN) 等情况
    if (+a !== +a) return +b !== +b;
    // 其他判断 ...
}

console.log(eq(a, b)); // true

基本实现

var toString = Object.prototype.toString;

function deepEq(a, b) {
    var className = toString.call(a);
    if (className !== toString.call(b)) return false;

    switch (className) {
        case '[object RegExp]':
        case '[object String]':
            return '' + a === '' + b;
        case '[object Number]':
            if (+a !== +a) return +b !== +b;
            return +a === 0 ? 1 / +a === 1 / b : +a === +b;
      case '[object Date]':
      case '[object Boolean]':
            return +a === +b;
    }

    // 其他判断
}

isEqualObject

/**
 * 用于判断是否为值相同对象(若有value为对象,则对指定key进行比较)
 *
 * @param {Object} x 目标对象
 * @param {Object} y 对比对象
 * @return {boolean} 比较结果,true表示相同,false为不同
 */
export const isEqualObject = (x = {}, y = {}) => {
    let inx = x instanceof Object;
    let iny = y instanceof Object;
    if (!inx || !iny) {
        return x === y;
    }

    if (Object.keys(x).length !== Object.keys(y).length) {
        return false;
    }

    for (let key in x) {
        let a = x[key] instanceof Object;
        let b = y[key] instanceof Object;
        if (a && b) {
            if (!isEqualObject(x[key], y[key])) {
                return false;
            }
        }
        else if (x[key] !== y[key]) {
            return false;
        }

    }
    return true;
};

FAQ

#faq/js

克隆

review-time: 3

克隆

类型判断

es-object

等值判断

es-object

创建一个空对象

Object.create(null) 与 {} 的区别:b={} 相当于 b=new Object,因此,bObject 构造函数的实例。

Object.create(null) 会创建一个空对象,它没有原型。

Freeze

如果真的想将对象冻结,应该使用 Object.freeze 方法。

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

上面代码中,常量 foo 指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

Keys 顺序

JS 中对象的 key 是有顺序的 - 知乎

map.keys 是有序的, 按照插入顺序