# api 进阶

抱着学习计算机科学中一门语言的态度,我开启了这一系列的记录。在普通的开发工作中可能并不会涉及到 javascript 每一个 api,但是我想做的 是极致性能的工具、框架、对这门语言的精通,所以为了更好的基础,在此记录 javascript 在发展中的 api 细节。

# Object

通常使用两种方式创建对象,对象字面量表示和构造函数。

let obj1 = {
  a: 1
}
let obj2 = new Object()

对于使用构造函数创建对象,可以接收参数,不同的参数创建的对象也是有区别的。

// 创建一个继承 Object.prototype 的空对象
let obj1 = new Object // 不推荐
let obj2 = new Object()
let obj3 = new Object(null)
let obj4 = new Object(undefined)
// 非构造函数调用
let obj5 = Object()
// 对象字面量
let obj6 = {}
// 会创建包装类型
let objNumber = new Object(1) // Number

// 会创建引用类型
let objObject = new Object({a: 1}) // Object
let objArray = new Object([1,2,3]) // Object

TIP

在 javascript 函数中,构造函数也是一个普通函数,可以调用。如果构造函数中含有 this ,那么只有在使用构造函数的情况才会 指向当前对象,否则指向全局对象。

# Object.prototype

几乎所有创建出来的 js 对象,都是 Object 实例。这些实例会继承 Object.prototype 上的属性和方法。但是 js 的机制是强大而 又存在风险的。可以覆盖或遮蔽原型链上的属性或方法,也可以创建不具有原型链的对象。

  • Object.create(null) # 可以创建一个没有原型的对象
  • 操作 Function.prototype
  • Object.setPrototypeOf

# Object.assign

这是我使用比较多的方法了,他可以将一个或多个源对象复制到目标目标对象并且返回目标对象。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(returnedTarget); // {a: 1, b: 4, c: 5}
console.log(target); // {a: 1, b: 4, c: 5}
console.log(returnedTarget === target) // true
// 对于需要拷贝属性设置,如 wtritable 需使用其他方法

对于继承属性和不可枚举属性是不能拷贝的

const obj = Object.create({foo: 1}, { // foo 是个继承属性。
    bar: {
        value: 2  // bar 是个不可枚举属性。
    },
    baz: {
        value: 3,
        enumerable: true  // baz 是个自身可枚举属性。
    }
});

const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }

TIP

只有 String 包装对象可能具有可枚举属性

const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo")

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4); 
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

遇到异常后续拷贝失败

const target = Object.defineProperty({}, "foo", {
    value: 1,
    writable: false
}); // target 的 foo 属性是个只读属性。

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。

console.log(target.bar);  // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo);  // 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz);  // undefined,第三个源对象更是不会被拷贝到的。

完整的拷贝需要配合 Object.getOwnpropertyDescriptor 和 Object.defineProperties 将属性描述符整个拷贝。

// TODO 拷贝可枚举的 symbol
const obj = {
      foo: 1,
      get bar() {
        return 2;
      }
    };
    
    // 下面这个函数会拷贝所有自有属性的属性描述符
    function completeAssign(target, ...sources) {
      sources.forEach(item => {
        let descriptors = Object.keys(item).reduce((descriptors, key) => {
          descriptors[key] = Object.getOwnPropertyDescriptor(item, key)
          return descriptors;
        }, {})
        console.log(descriptors)
        Object.defineProperties(target, descriptors)
      })
      return target
    }

    let copy = completeAssign({}, obj);
    console.log(copy);

polyfill 不支持 Symbol

if (typeof Object.assign != 'function') {
  // Must be writable: true, enumerable: false, configurable: true
  Object.defineProperty(Object, "assign", {
    value: function assign(target, varArgs) { // .length of function is 2
      'use strict';
      if (target == null) { // TypeError if undefined or null
        throw new TypeError('Cannot convert undefined or null to object');
      }

      let to = Object(target);

      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) { // Skip over if undefined or null
          for (let nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}

# Object.create

Object.create 创建一个新的对象,他使用一个已经存在的对象作为原型。

var person = {
  isHuman: true
}

var me = Object.create(person)
me.isHuman // true
参数与 类型 是否必填
proto Object true
propertiesObject properties false

propertiesObject 的参数对应 Object。defineProperties 的第二个参数,代表创建对象的可枚举属性,而不是原型属性。

可以使用 Object.create(null) 创建一个不含有原型的空对象。

# Object.defineProperty

defineProperty 是 es5 加入的一个对象方法,此方法可以在一个对象上新增一个属性或者修改现有属性,并返回这个对象。

# 属性描述符

defineProperty 接收三个参数,obj, prop, descriptor。obj 是想要修改的对象,prop 是想要增加或修改的属性,descriptor 是属性描述符。 属性描述符有两种,数据描述符和访问器描述符。在《javascript高级程序设计第三版》中,把他叫做属性类型。

# 数据描述符

在 js 高程中说 这些特性是为了实现 javascript 引擎用的,因此不能通过 javascript 直接访问。 但是其实可以通过 Object.getOwnProperties 访问这些属性。

  • [[Enumerable]] 是否可以for...in...枚举
  • [[Configurable]] 是否可删除(实际作用效果我觉得大于这个定义)
  • [[Writable]] 是否可修改 Value
  • [[Value]] 属性的值
var person = {
  name:'Richard'
}
// 默认属性描述符为:Enumerable:true,Configurable:true,Writable:true,Value:'Richard'

如果使用 Object.defineProperty 创建一个属性,那么所有特性默认是 false 。如果 configurable 的值是 false ,将无法再通过 Object.defineProperty 设置属性。

var person = {
  name:'Richard'
}
Object.defineProperty(person,'fullname',{
  configurable:false
})
// 无效
Object.defineProperty(person,'fullname',{
  configurable:true,
  writable:false
})

想修改默认属性描述符,必须通过 Object.defineProperty

WARNING

IE8 实现了definePropery 但是只存在 DOM 中,并且只能使用访问器描述符,所以避免在 IE8 下使用。

这些特性不一定是自身属性,也有可能来自继承,所以如果想设置默认值,需要冻结 Object.prototype ,或者通过 Object.create(null) 将 proto 属性指向 null 。

# 访问器描述符

  • [[Configurable]]
  • [[Enumerable]]
  • [[Get]] 默认值 undefined
  • [[Set]] 默认值 undefined
var person = {
  _name: 'Richard'
}
Object.defineProperty(person,'name',{
  get: function() {
    return this._name
  },
  set: function(newV) {
    this._name = newV
  }
})
person.name = 'Kame'

属性描述符不一定是自身属性,可以通过继承得到,因此在某些情况需要通过 Object.freeze 确保默认的属性描述符,或者更好的做法是创建一个没有原型 的对象,确保属性描述符的确定性。

// 通过 Object.create(null) 创建具有默认属性描述符的属性,如果使用对象字面量 var obj = {} 
// 通过更改原型可以改变默认的属性描述符   Object.prototype.configurable = true
var obj = Object.create(null)
var descriptor = Object.create(null)
Object.defineProperty(obj, 'key', descriptor) // configurable: false enumerable: false value: undefined writable: false

// 显示创建
var obj = Object.create(null)
Object.defineProperty(obj, 'key', {
  configurable: false,
  enumerable: false,
  value: undefined,
  writable: false
})

// 冻结原型
var obj = {}
;(Object.freeze||Object)(Object.prototype)
// Error in mounted hook: "TypeError: Cannot add property configurable, object is not extensible"
Object.prototype.configurable = true
Object.defineProperty(obj, 'key', {})
console.log(Object.getOwnPropertyDescriptors(obj))

重复使用同一个对象,可以减少对象创建

// 循环使用同一对象
function withValue(value) {
  var d = withValue.d || (
    withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value: null
    }
  );
  d.value = value;
  return d;
}
Object.defineProperty(obj, "key", withValue("static"));

# Object.entries

Object.entries接收一个可迭代对象,返回给定对象可枚举属性的键值对数组。

Object.entries([1,2])
// [Array(2), Array(2)]
// 0: (2) ["0", 1]
// 1: (2) ["1", 2]

# Object 转 Map

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

# String.prototype.replace

TIP

str.replace(regexp|substr, newSubStr|function)

当第二个参数传入一个函数时,函数会直接执行,函数的返回值会替换匹配到的结果。当第一个参数是正则表达式时,并且是全局匹配模式,那么函数会执行多次。 如果正则中含有括号匹配,那么相应的函数内会增加括号对应的参数,具体如下:

// 如果匹配模式是:
/(\a+)(\b+)/
replacer(match,p1,p2,ofsset,string)
// 其中 match 是匹配的字符串,p1 是第一个括号内的匹配,p2 ... offset 是子字符串在原字符串中的偏移量(也就是原来的 index 值),string
是原字符串。

# void

# 代替 undefined

undefined 并不是保留字。在某些老旧的浏览器中,undefined 可以被改写,另外在局部作用域下也可以被改写,另外可以节省几个字节。

!function() {
  var undefined = 8
  var a = undefined
  console.log(a) // 8
}()

# iife (Immediately Invoked Function Expressions)

void 是实现 iife 的方式之一

(function(p){
    // 函数体内容
})(p);

(function(p){
    // 函数体内容
}( p));

!function(p){
    // 函数体内容
}(p)

+function( p){
    // 函数体内容
}(p)

-function(p){
    // 函数体内容
}(p)

~function(p){
    // 函数体内容
}(p)

void function(p){
    // 函数体内容
}(p)

# 消除副作用

当使用箭头函数时,可以返回 void 表达式,确保表达式 api 变更时不会影响到使用。

// doSomething 更改为返回 true 时不会影响到 button.onclick
button.onclick = () => void doSomething();

# RegExp

# 创建

  • var re = /ab+c/; // 当模式确定时,使用这种方式可以提升性能
  • var re = new RegExp('ab+c'); // 当传入动态模式需要使用这种方式

# 转换方法

# toString() toLocalString() 与 valueOf()

每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。

// 也被称作隐式类型转换
var a = 1 + '2' // "12"
null + '' // "null"

# 不同数据类型下的表现

# Null Undefined

因为 toString() 是 Object 对象方法,null 指向空,内存中也没有生成任何对象。

null.toString() // VM29413:1 Uncaught TypeError: Cannot read property 'toString' 
                // of null at <anonymous>:1:6

TIP

但是 nullable(比如 number?) 是可以使用 toString() 的,因为虽然可以是 null ,但实际上此时的 null 是有类型的,只不过值是空。 强制类型转换 String() 兼容 Null 和 Undefined ,其余的转换结果是相同的。

undefined 和 null 原理是一样的。

# String

字符串在使用 toString() 没有任何问题,因为返回的就是它本身的值。

# Number

1.toString() // Uncaught SyntaxError: Invalid or unexpected token
1 .toString() // "1"
1..toString() // "1"
(1).toString() // "1"

对于 1 来说,javascript 引擎无法判断这里的 . 是 float 型的 . 还是对象的操作符 . 。1 (空格)和 () 运算符效果相似,他们都会 返回表达式的值,然后会被包装成为 Number 类,Number 类重写了 Object 的 toString 方法。对于 1.. , javascript 的 float 类型允许存在 1. 这种写法,这样就可以判断出第二个 . 一定是对象的操作符 .

# Boolean

true.toString() // tue

javascript 引擎可以直接识别出 truefalse,并转换为包装类型 Boolean,Boolean 类重写了 Object 的 toString 方法。

# Object

...

# forEach

arr.forEach(callback[, thisArg]);

# callback

为数组中每个元素执行的函数,该函数接收三个参数:

# currentValue

数组中正在处理的当前元素。

# index可选

数组中正在处理的当前元素的索引。

# array可选

forEach() 方法正在操作的数组。

# thisArg可选

可选参数。当执行回调函数时用作 this 的值(参考对象)。

TIP

callback 方法可以使用箭头函数,不改变 this 指向。后两个不常用的参数,array 就是当前遍历的数组 (===),而最后的 thisArg可以改变当前对象。 但是一单使用箭头函数,第三个参数就会失效。

# 密集数组和稀疏数组

let arr1 = new Array(10)
// 并不会执行
arr1.forEach(item=>{
  console.log(item)
})

let arr2 = Array.apply(null, Array(10))
arr2.forEach(item=>{
  console.log(item)
})

TIP

创建数组,Array() 和 new Array() 是一样的。(Ecmascript 标准) When Array is called as a function rather than as a constructor, it creates and initialises a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments.

forEach 会忽略稀疏数组中的空项(已删除或未初始化)。

# 跳出循环

forEach 不能跳出循环,除非抛出一个异常。

let arr = [{name:'a'},{name:'b'},{name:'c'},{name:'d'},{name:'e'}]
arr.forEach((item,index,a) => {
        if(item.name === 'a') return
        console.log(item)
      },new Object())

# 已访问的元素在迭代时被删除了

由于执行到 name === 'b' 时,第一项被删除了,这时候每一项会向上平移,在这之后的每一项输出当前的项就提前了一位。

arr.forEach((item,index,a) => {
        if(item.name === 'b') arr.shift()
        console.log(item);
      },new Object())
/*0: {name: "a"}
1: {name: "b"}
2: {name: "d"}
3: {name: "e"}*/

# 一些不解

forEach 遍历的范围在第一次调用 callback 前就会确定。调用 forEach 后添加到数组中的项不会被 callback 访问到。如果已经存在的值被改变, 则传递给 callback 的值是 forEach 遍历到他们那一刻的值。已删除的项不会被遍历到。

# 选择器

# Document​.query​SelectorAll