ES6-对象方法及函数

2019/10/7 JSES6

🌙 1. 对象表示法

🌙 1.1 属性简洁表示法

在大括号{变量或函数}里面,直接写入变量和函数,作为对象的属性和方法

// ES6 属性表示法:
const name = 'Jax';
const age = 15;
const p2= {
    name,  // 属性简写
    age,
    sayHi(){ // 方法简写
        console.log('hello ' + this.name);
    }
}
1
2
3
4
5
6
7
8
9
10

🌙 1.2 属性名表达式

把表达式放在方括号内[表达式],作为对象属性名:

//restful Api
const method = ['get', 'post', 'put', 'delete'];
const getData = {
    [method[0]]() {
        console.log('GET');
    },
    [method[1]]() {
        console.log('POST');
    },
    [method[2]]() {
        console.log('PUT');
    },
    [method[3]]() {
        console.log('DELETE');
    },
    ['hi']: 'HeLLO';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 注意,属性名表达式与简洁表示法,不能同时使用,会报错。

  • 注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object]

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

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

myObject // Object {[object Object]: "valueB"}
1
2
3
4
5
6
7
8
9

所以,属性名表达式的值,必须为基本类型,最好为字符串。

🌙 2. 对象的属性

🌙 2.1 函数的name属性

函数的name属性,返回函数名:

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

person.sayName.name   // "sayName"
1
2
3
4
5
6
7

匿名函数:

(function(){console.log(11)}).name; // ''空字符串
let a = (function(){console.log(11)})();
a.name; // 'a'

(new Function()).name; // "anonymous"
1
2
3
4
5

bind函数:

let test = function(){};
test.bind().name; // "bound test"
1
2

Symbol属性名的函数:

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

cost key = Symbol('key');
let gen = {
    [Symbol.iterator](){
        return this;
    },
    [key](){};
};

gen[Symbol.iterator].name; // "[Symbol.iterator]"
gen[key].name; // "[key]"
1
2
3
4
5
6
7
8
9
10

🌙 2.2 属性的可枚举性和遍历

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }
1
2
3
4
5
6
7
8

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

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

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。(包含继承)
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

比如,对象原型的toString方法,以及数组的length属性,就通过“可枚举性”,从而避免被for...in遍历到。

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

Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
1
2
3
4
5

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

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

大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。

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

(1)for...in

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

(2)Object.keys(obj)

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

(3)Object.getOwnPropertyNames(obj)

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

(4)Object.getOwnPropertySymbols(obj)

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

(5)Reflect.ownKeys(obj)

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

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

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
1
2

上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性210,其次是字符串属性ba,最后是 Symbol 属性。

🌙 3. super关键字

  • this关键字总是指向函数所在的当前对象

  • super指向当前对象的原型对象,只能用在对象的方法之中

Object.setPrototypeOf(),为现有对象设置原型,返回一个新对象 接收两个参数:第一个是现有对象,第二是原型对象。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};
// 指定原型之前
obj.__proto__ === proto;//false
obj.find() // undefined
// 指定原型之后
Object.setPrototypeOf(obj, proto);
obj.__proto__ === proto ;//true
obj.find() // "hello"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);// this会动态绑定
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo(); // proto.foo
  }
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

super在面向对象编程一节会在专门学习。

🌙 4. 扩展运算符

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}

{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
1
2
3
4
5
6
7
8
9
10

完整克隆一个对象,并拷贝对象原型的属性:

扩展运算符的解构赋值,不能复制继承自原型对象的属性

let obj = {
    a:1,
    b:2,
    c: [1,2]
};
let proto = {
    d:3,
    e: 4
}
// 指定原型
Object.setPrototypeOf(obj, proto);

// 写法一:浅拷贝,没有继承原型
obj1 = {...obj};
obj1.d; //undefined

// 写法二: 浅拷贝,继承原型
const clone1 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
);
clone1.d; // 3
// 写法三: 浅拷贝,继承原型
const clone2 = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)
clone2.d; // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

合并对象:

let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);
1
2
3

🌙 5. 函数

🌙 5.1 默认参数

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
1
2
3
4
5
6
7

注意,参数变量是默认声明的,所以不能用letconst再次声明。

function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"
1
2
3
4
5
6

🌙 5.2 函数的length属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
1
2
3

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。rest 参数也不会计入length属性

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
(function(...args) {}).length // 0
1
2
3

🌙 5.3 rest参数

rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象。rest 参数之后不能再有其他参数

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
sortNumbers([1,2,3,4])
1
2
3
4
5
6
7
8

ES6规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

🌙 5.4 箭头函数

ES6 允许使用“箭头”(=>)定义函数。

// 箭头函数写法
[1,2,3].map(x => x * x);
1
2

箭头函数有几个使用注意点:

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

function foo() {
  setTimeout(() => {
    console.log('args:', arguments);
  }, 100);
}

foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
1
2
3
4
5
6
7
8

上面代码中,箭头函数内部的变量arguments,其实是函数fooarguments变量。

另外,由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向。

🌙 5.5. 函数的尾调用及尾递归

尾调用(Tail Call):指某个函数截止执行的最后一步是return另一个函数,且没有其他操作。

function f(x){
    // 尾递归即在程序尾部调用自身,注意这里没有其他的运算
  return g(x);
}

// 以下不属于尾调用
// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1;
}

// 情况三
function f(x){
  g(x);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。

如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

尾调用优化(Tail call optimization):即只保留内层函数的调用帧

注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}
1
2
3
4
5
6
7

上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效

尾递归:函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120
1
2
3
4
5
6

递归本质上是一种循环操作。

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}

sum(1, 100000) 
// Uncaught RangeError: Maximum call stack size exceeded(…)
1
2
3
4
5
6
7
8
9
10

上面代码中,sum是一个递归函数,参数x是需要累加的值,参数y控制递归次数。一旦指定sum递归 100000 次,就会报错,提示超出调用栈的最大次数。

蹦床函数(trampoline)可以将递归执行转为循环执行。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}
1
2
3
4
5
6
function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}
1
2
3
4
5
6
7

现在,使用蹦床函数执行sum,就不会发生调用栈溢出:

trampoline(sum(1, 100000))
// 100001
1
2

蹦床函数并不是真正的尾递归优化(Tail Call Optimisation),下面的实现才是:

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];
// 闭包
  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用上面的函数包装一下:


// sum使用尾递归优化
var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

// 没有造成栈溢出
sum(1, 100000)
// 100001
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码,很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。这就是尾递归优化。

🌙 7. Proxy对象

🌙 8. Reflect对象