介绍
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 还可以 set 一些 config 的. 通过 defineProperty 来配置.
Object.defineProperty(obj, 'age', { writable: true, enumerable: true, configurable: true, value: 11, get() { return 11; }, set(value) {}, });
writable 表示属性是否可以 set value. 如果设置成 false, 当 assign value to property 时 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 就不可以有 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)
values 可以获取所以属性值
注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
当访问属性值时, e.g. 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
以前 AngularJS 的 $scope 就使用了 prototype 概念, 导致了许多人在赋值的时候经常出现 bug. 原因就是上面这样.
你 get from prototype 不代表就是 set to prototype, 因为属性是可以动态添加的. 你以为是 set, 结果变成了 new property.
Function
介绍
JavaScript 的 Function 比较混乱. 尤其是 es6 之前. 因为它有双重身份.
第一个身份是直观的函数. 调用, 传参数, 获取返回值.
第二个身份是充当面向对象的 class. 这个比较不好理解.
这一 part 我们主要先看看它的第一个身份. class 的部分下一个 part 才讲. (不然很乱的...)
函数出没的地方
函数是一等公民, 你可以直接定义它
function myFunction1(param1){ return 'return value'; }
可以 assign 给变量
const myFunction2 = function (param1){ return 'return value'; };
可以当参数传
[].map(function (value, index) { return 'value'; });
可以当函数返回值
function myFunction1() { return function () {}; }
可以当对象的方法
const obj = { method: function (param1) { return 'value'; }, };
宽松的参数数量
参数的数量是没有严格定义的.
举例, 函数声明了一个参数, 但是调用时传入超过 1 个参数是 ok 的
function myFunction1(param1) { console.log(param1); // 1 } myFunction1(1, 2, 3);
而且参数是 optional 呢, 调用时没有传参数也是 ok 的
function myFunction1(param1) { console.log(param1); // undefined } myFunction1(); myFunction1(undefined); // 和上一行是等价的
参数的默认值 (es6)
function myFunction1(param1 = 'default value') { console.log(param1); } myFunction1(undefined); // log 'default value' myFunction1(); // log 'default value' myFunction1('value'); // log 'value'
用等于设置 defualt value
arguments 对象
当参数数量不固定时, 可以通过 arguments 获取最终调用者传入的所以参数信息
function myFunction1(param1 = 'value') { console.log(arguments[0]); // a console.log(arguments[1]); // b console.log(arguments[2]); // c console.log(arguments.length); // 3 } myFunction1('a', 'b', 'c'); myFunction1(); // arguments.length = 0 (arguments 不看 default value)
类似 C# 的 params 关键字
宽松的返回值
即使函数没有返回值, 但调用者还是可以把它 assign 给变量, 默认的返回值是 undefined. 这个和参数 undefined 概念是同样的.
function myFunction1() {} const returnValue = myFunction1(); // undefined
method 中的 this
对象的成员是函数的话, 我们会把称它为方法 (method). 它和普通函数有一点点的不一样.
const obj = { value: 'value', method1: function () { console.log(this.value); // value }, };
method 中的 this 指向的是当前的对象 obj. 这有一点点 class 的概念了.
在一些 build-in 的接口中也可以看到这类用法
onreadystatechange 是 xhttp 对象的 method, method 中的 this 指向的是 xhttp 对象.
this 的 偷龙转风 call & apply & bind
method 中的 this 是可以通过一些手法调换的. 比如下面这样
const obj = { value: 'value', method1: function () { console.log(this.value); // another value }, }; const otherObj = { value: 'another value' }; obj.method1.call(otherObj);
使用 call, apply, bind 都可以在调用 method 的时候传入一个对象来替代 this. 这是一种灵活的玩法. 但也相对混乱. es6 以后越来越少看到这种玩法了.
第一是作为函数
就是拿来调用, 传参数, 拿返回值的函数.
function myFunction(param1) { return 'return value'; }
第二是作为对象
函数也是对象, 所以它有属性.
function myFunction(param1) { return 'return value'; } console.log('name', myFunction.name); console.log('toString', myFunction.toString()); console.log( 'prototype', Object.getPrototypeOf(myFunction) === Function.prototype );
log
第二是作为 class (这一 part 我主要讲作为函数的函数, class 的部分下一个 part 讲)
es6 以后.
作为函数的部分可以用箭头函数取代
作为 class 的部分, 可以用 class 取代
所以 es6 以后比较少看到 function 这个字眼了, 也避免了混乱, 但是 es6 class 只是语法糖来的哦, 它底层依然时 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 对象的哦.