JavaScript – Object, Prototype, Function, Class


介绍

JavaScript 有 4 个概念一定要搞清楚.

Object, Prototype, Function, Class

简单说一下:

Object

C# 很少直接用 object, 通常都是会开一个 class, new class 出 object

但是 JavaScript 却不同, JavaScript 更多的是直接开一个 object, 比较少开 class. 而且 JavaScript 的 Object 有很多好玩的特性.

Prototype

原型链是很好的一种设计, 但坑也不少. JavaScript class 的继承就是靠 prototype 完成的, 当然 prototype 其实还可以做其它的事情.

Function

JavaScript 的 function 除了是 function, 还带有 class 的特性. 比如它可以 new

Class

JavaScript 的 class 是假的, 语法糖来的. 它外部和 C# 的 class 特性一样, 但背地里它是用 Function Prototype Object 的特性实现的.

Object

create object

const obj = {
  name: 'Derrick',
  age: 11,
};

get / set property value

console.log(obj.name); // get property value
obj.age = 13;          // set property value

new / delete property

对象的属性是可以动态添加和删除的

obj.lastName = 'Yam';  // new property
delete obj.lastName;   // delete property

getter in object

甚至可以直接定义 getter 属性

const obj = {
  firstName: 'Derrick',
  lastName: 'Yam',
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
};

Object.defineProperty

对象的 property 是有一些 config 的. 通过 defineProperty 来配置.

Object.defineProperty(obj, 'age', {
  writable: true,
  enumerable: true,
  configurable: true,
  value: 11,
  get() {
    return 11;
  },
  set(value) {},
});

writable 表示属性是否可以 set value. 如果设置成 false, 当调用 obj.age = 15 的时候, age 的值不会有任何改变 (它虽然不会报错, 但操作被无视掉了)

enumerable 表示属性是否能被遍历 (下面会讲到如何遍历属性), false 就是说不能被遍历

configurable 表示是否可以被 defineProperty. 一旦设置成 false 就不能改回来了. lock 死掉了.

get 就是 getter 方法

set 就是 setter 方法. e.g. obj.age = 15 的时候触发, value 就是 15

value 就是属性值咯

注1: writable, enumerable, configurable 默认值都是 false, value 是 undefined

注2: Cannot both specify accessors and a value or writable attribute (有 getter setter 就不可以 set value 和 writable)

getter setter

完整的 getter setter 长这样

Object.defineProperty(obj, '_age', {
  writable: true,
  enumerable: false, // 不允许被遍历
  configurable: true,
  value: 0,
});

Object.defineProperty(obj, 'age', {
  enumerable: true,
  configurable: true,
  get() {
    console.log('拦截 getter');
    return this._age;
  },
  set(value) {
    console.log('拦截 setter');
    this._age = value;
  },
});
obj.age = 15;
console.log(obj.age);

_age 是 "private 属性"

遍历 object

Object.keys(obj); // ['firstName', 'lastName', 'fullName']
Object.entries(obj); // [['firstName', 'Derrick'], ['lastName', 'Yam'], ['fullName', 'Derrick Yam']]
for (const [key, value] of Object.entries(obj)) {}

keys 可以获取所有属性名

entries 可以获取属性和值 (es2017 才有)

注1: 属性必须是 enumerable: true 才能被遍历

注2: 属性是 Symbol 的话, 是无法被遍历的

注3: object 不是 iterator 所以不能直接使用 for of obj, 必须用 Object.keys 或者 Object.entries

如果想获取到 enumerable: false 的属性或者 Symbol 属性, 需要使用下面这 2 个方法.

Object.getOwnPropertyNames(obj);
Object.getOwnPropertySymbols(obj);

 

Prototype

用 Chrome DevTools 打开 Object 会发现它有一个特别的属性叫 Prototype

原型链的查找过程

prototype 有啥用呢?

prototype 也是一个对象, 可以这么理解, 一个对象里头, 又连着另一个对象.

举例: 对象 child 连着对象 parent

当 child.prop 的时候, JavaScript 会先去找 child 对象中是否有 prop 这个属性. 有的话就返回它的值.

如果没有这个属性, 那么它会接着去 prototype parent 寻找这个属性. 有的话就返回它的值.

如果还是没有那就再去 parent 的 prototype 找, 一直找直到某个 prototype = null 才结束.

指定 Prototype

const obj = {};
const objPrototype = Object.getPrototypeOf(obj);
console.log(objPrototype === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

当我们创建一个对象时, 它默认就有连着一个 prototype. 那就是 Object.prototype

通过 Object.getPrototypeOf 可以获取到某个对象的 prototype.

Object.prototype 定义了一些 hasOwnProperty 之类的方法. 所以虽然 obj = {} 看上去是空的, 但却可以调用 obj.hasOwnProperty 方法. 就是因为原型链查找的缘故.

通过 Object.create 可以在创建对象时指定其 prototype

const myPrototype = {
  age: 11,
};
const obj = Object.create(myPrototype);
console.log(obj.age); // 11

原型链: obj > myPrototype > Object.prototype

动态替换 prototype

Object.setPrototypeOf(obj, myPrototype);

 遍历 prototype 属性

上面介绍的 Object.keys, Object.entries 都只能遍历当前对象的属性. 

for in 除了可以遍历对象属性还可以遍历出所有 prototype 链上的属性 (必须是 enumerable: true)

for (const key in obj) {
  const isFromPrototype = !obj.hasOwnProperty(key); // 判断是当前对象还是 prototype 属性
}

小心坑

看注释

const parent = {
  age: 11,
  fullName: {
    firstName: '',
    lastName: '',
  },
};
const child = Object.create(parent);
console.log(child.age); // read from parent
child.age = 15;         // 注意: 这里不是 set property to parent 而是 new property to child
console.log(child.age); // 注意: read from child

console.log(child.fullName.firstName); // read from parent
child.fullName.firstName = 'Derrick';  // 注意: 这里是 set property to parent, 而不是 new property to child 哦 (和上面不同)
console.log(child.fullName.firstName); // 注意: still read from parent

当前 AngularJavaScript 的 $scope 就使用了 prototype 概念, 导致了许多了在赋值的时候经常出现 bug. 原因就是上面这样. 

你 get from prototype 不代表就是 set to prototype, 因为属性是可以动态添加的. 你以为是 set, 结果变成了 new property

Function

介绍

JavaScript 的 Function 有双重身份

第一是作为函数

第二是作为 class

es6 以后.

作为函数的部分可以用箭头函数取代

作为 class 的部分, 可以用 class 取代

所以 es6 以后比较少看到 function 这个字眼了, 也避免了混乱.

函数参数与返回

函数的参数是没有规定的, 看看下面的例子

function myFunction(arg0 = 'default value', arg1, arg2) {
  console.log(arguments); // [undefined, 2, 3, 4, 5];
  console.log(arg0); // default value
  return 'value';
}
myFunction(undefined, 2, 3, 4, 5);

函数只声明了 3 个参数, 但是调用的时候你可以传入 5 个, 传多传少都可以.

传少获取的时候值是 undefined, 传多可以忽略, 也可以通过 arguments 对象获取信息. 

参数 + default value = optional 参数. 而且它不像 C# 那样强制你必须把 optional 放后面. 

JS 在调用函数时, 传入 undefined 就表示没有传入一样. 下面 2 句是等价的

myFunction();
myFunction(undefined, undefined, undefined, undefined);

返回值也是 optional 的, 没有返回, 调用者会获取到 undefined 值.

总结: 参数, 返回值 undefined 就表示没有传入参数和没有返回值.

es6 函数参数

arguments 虽然厉害但是也相对不好理解. es6 会用 rest parameters 来取代 arguments 的作用.

function myFunction(arg0 = 'default', ...otherArgs) {
  console.log(arg0); // default
  console.log(otherArgs); // [1, 2]
  return 'value';
}
myFunction(undefined, 1, 2);

另外箭头函数内是没有 arguments 对象的哦.

相关