类和构造函数
// 任何函数都可以认为也可以作为构造函数
function Foo(x) { // 构造函数
this.x = x; // 设置实例字段初始值, 一般在这里声明和初始化实例字段, 注意必须使用 this 访问实例属性. 该字段只属于实例, 与类无关
this.f = function() {return 'f'}; // 定义实例方法, 这样定义会导致每次 new 都执行一次, 而且只属于实例, 与类无关, 后期无法通过修改 Foo.prototype.foo 来更改其动作
};
// 构造函数本身的属性不会被继承, 所以是类独有
// 构造函数本身的属性是类字段
// 构造函数本身的函数是类方法
Foo.classField = 1; // 类字段
Foo.classFunction = function() {}; // 类方法
// 构造函数在使用 new 关键字被用于构造的时候, 其 prototype 会被实例继承(但注意不是将实例的 prototype 指向构造函数)
// 每个函数都一定具有默认的 prototype 属性(注意是函数, 并不是所有一般的对象)
// 构造函数 prototype 属性中的属性是实例字段
// 构造函数 prototype 属性中的函数是实例方法
Foo.prototype.instanceField = 1; // 可以在这里定义实例字段, 不过一般不这样做
Foo.prototype.instanceFunction = function() {}; // 实例方法, 存在与类中, 通过原型链查找到并访问, 这样定义不会每次被执行, 而且后期可以通过修改 Foo.prototype.instanceFunction 来改变其动作
原型(prototype) 和 prototype 属性
JavaScript 中一切都是对象, 而所有对象都有原型, 英文是 prototype. 可以用标准的 Object.getPrototypeOf(o)
方式, 或者在某些非标准实现中使用 o.__proto__
查看对象的原型(prototype).
另外需要注意的是, 任何一个函数都有一个 prototype 属性, 注意属性二字, 虽然此处也叫 prototype, 拼写跟原型的 prototype 一样, 但其实根本是两个概念:
- 原型(prototype): 任意对象都有原型(prototype), 这个 prototype 是使用来实现继承链向上查找用的, 这个 prototype 也就是
o.__proto__
. - prototype 属性: 对于函数, 特别是用于构造用的函数, prototype 属性才有意义, 这个 prototype 属性, 在使用
new
关键字构造对象的时候, 会自动成为对象的原型(prototype), 同样也是对象的o.__proto__
.
创建新对象
- 对象直接量:
o = {}
. - 构造函数:
o = new Foo()
. 此时o
的原型是Foo.prototype
, 即o.__proto__ === Foo.prototype
=>true
. - Object.create() 方法:
o = Object.create(Array.prototype)
. 此时o
的原型被设置为Array.prototype
, 即o.__proto__ === Array.prototype
=>true
.
此方式主要可用于继承, 也就是可用于创建一个对象的 prototype 属性, 比如 Bar.prototype = Object.create(Foo.prototype)
, 此时因为 Bar.prototype
属性的原型是 Foo.prototype
属性, 即 Bar.prototype.__proto__ === Foo.prototype
=> true
, 所以可认为 Bar
是 Foo
的子类.
意外的全局变量
在非严格模式下, 如果没有加 var
会创建一个全局变量(这也太扯了), 加了 var
才是局部变量:
(function foo() {
var real_local = 1;
non_local = 2;
}());
console.log(real_local); // error, not defined
console.log(non_local); // 2
关于立即执行函数(IIFE)
正常情况下, 使用 function
关键字得到的是一个函数声明语句. 注意函数声明语句是没有值的, 我们不能使用它. 能够被调用的一定是一个值, 比如可以是一个函数表达式, 因为表达式是一定有值的.
如果我们直接在一个函数声明语句后面跟一个括号尝试调用它会发生什么?
function(){}(); // 语法错误, 函数声明语句需要一个函数名
那么我们给它一个函数名可以吗?
function foo(){}(); // 语法错误, 括号内需要一个表达式
// 上面的写法等效于:
function foo(){}; // 没有语法错误, 一个函数声明语句
(); // 语法错误, 不能单独使用括号
那么我们给括号内传一个参数呢?
function foo(){}(1); // 没有语法错误, 但是不是我们想要的效果
// 上面的写法等效于:
function foo(){}; // 没有语法错误, 一个函数声明语句, 没有函数调用
(1); // 没有语法错误, 但是并未调用函数
解决办法就是强制告诉解释器, 我们这里的并不是函数声明语句, 而是函数表达式.
因为括号内是不允许出现声明语句的(敲黑板, 重点), 所以出现在括号内的一定是函数表达式, 所以我们可以这样写:
(function(){}()); // 函数表达式, 且立即得到调用, 有返回值则整体作为括号的返回值
(function(){})(); // 函数表达式, 作为括号的返回值, 并在括号外得以调用
在不关心函数返回值的情况下, 还有一些奇怪的写法, 只要告诉解释器, 此处是一个函数表达式即可:
+funciton(){}();
-funciton(){}();
!funciton(){}();
~funciton(){}();
new funciton(){}();
ES6一些新特性
解构赋值
let [a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
const [a, b, c, d, e] = hello;
console.log(a); // "h"
console.log(b); // "e"
console.log(c); // "l"
console.log(d); // "l"
console.log(e); // "o"
扩展运算符
let a = [1, 2, 3];
console.log(a); // 原样: [1, 2, 3]
console.log(...a); // 展开: 1, 2, 3
解构赋值与扩展运算符同时使用
let a = [1, 2, 3];
let [...b] = a; // b 的每一位被展开, 依次被赋值为 a 中的每一位. b 不是 a 的别名
a[0] = 999;
console.log(a); // [999, 2, 3]
console.log(b); // [1, 2, 3]
// 其实以下写法更好理解
let b = [...a]; // a 展开后放入数组, 赋值给 b
JavaScript 权威指南中提到的一些函数
- o.isPrototypeOf()
- o.hasOwnProperty()
- Object.defineProperty(), Object.defineProperties()
- function.bind(o) ==> 让我想到了 Python 中的 __get__()
笔记
- 全局变量是全局对象地属性. 局部变量被当作函数调用相关地某个对象地属性, 被称作"调用对象", 或者"声明上下文对象".
函数肯定会作为方法被某个对象调用, 作为这个对象的属性. 如果没有明确的对象, 那么就不是方法, 而是函数, 也没有特定的对象, 那么也就只能是全局对象了. 此时也就是全局变量.
---- JavaScript权威指南 page 58.
- 对象创建表达式创建一个对象并调用一个函数(即构造函数)来初始化这个对象的属性. JavaScript 先创建一个新的空对象, 这个空对象被当作 this 的值来调用这个构造函数, 构造函数可以使用 this 来初始化对象属性. 构造函数不返回值, 这样新创建的对象就是对象创建表达式的值, 否则返回的这个对象成为对象创建表达式的值, 而新创建的对象则被抛弃了.
---- JavaScript权威指南 page 65.
其实不只是创建空对象, 使用 new 关键字的时候, 这个空对象的原型会被自动设置为构造函数的 prototype 属性.
- ECMA-262 把对象定义为: "无序属性的集合, 其属性可以包含基本值, 对象或者函数". 我们可以把 ECMAScript 的对象想象成散列表: 无非就是一组名值对, 其中值可以是数据或者函数.
---- JavaScript高级程序设计 page 138.
所以, JavaScript 的对象就是散列表或者叫字典, 字典也就是对象, 而且不但如此, 数组其实也是对象(字典), 函数其实也是对象(字典), JavaScript 里的基本一切都是对象(字典).