类型
这 7 种语言类型是:Undefined;Null;Boolean;String;Number;Symbol;Object。
JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是 JavaScript 语言公认的设计失误之一,所以,我们为了避免无意中被篡改,我建议使用 void 0 来获取 undefined 值。
Null 类型也只有一个值,就是 null,它的语义表示空值,与 undefined 不同,null 是 JavaScript 关键字,所以在任何代码中,你都可以放心用 null 关键字来获取 null 值。
String 有最大长度是 2^53 - 1
因为 String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。
JavaScript 中的 Number 类型有 18437736874454810627(即 2^64-2^53+3) 个值。
JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是 JavaScript 为了表达几个额外的语言场景(比如不让除以 0 出错,而引入了无穷大的概念),规定了几个例外情况:NaN,占用了 9007199254740990,这原本是符合 IEEE 规则的数字;Infinity,无穷大;-Infinity,负无穷大。
console.log( 0.1 + 0.2 == 0.3); // false
浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。
我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为: var o = new Object o[Symbol.iterator] = function() { var v = 0 return { next: function() { return { value: v++, done: v > 10 } } } }; for(var v of o) console.log(v); // 0 1 2 3 ... 9代码中我们定义了 iterator 之后,用 for(var v of o) 就可以调用这个函数,然后我们可以根据函数的行为,产生一个 for…of 的行为。
JavaScript 对象的特征
对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。 对象有状态:对象具有状态,同一对象可能处于不同状态之下。 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
var o2 = { a: 1 };
console.log(o1 == o2); // false
先来说第一类属性,数据属性。它比较接近于其它语言的属性概念。数据属性具有四个特征。 value:就是属性的值。 writable:决定属性能否被赋值。 enumerable:决定 for in 能否枚举该属性。 configurable:决定该属性能否被删除或者改变特征值。
第二类属性是访问器(getter/setter)属性,它也有四个特征。 getter:函数或 undefined,在取属性值时被调用。 setter:函数或 undefined,在设置属性值时被调用。 enumerable:决定 for in 能否枚举该属性。 configurable:决定该属性能否被删除或者改变特征值。
JavaScript 中对象独有的特色是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力
var o = { a: 1 };
o.b = 2;
console.log(o.a, o.b); //1 2
我们通常用于定义属性的代码会产生数据属性,其中的 writable、enumerable、configurable 都默认为 true。我们可以使用内置函数 getOwnPropertyDescriptor 来查看
var o = { a: 1 };
o.b = 2;
//a和b皆为数据属性
Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}
如果我们要想改变属性的特征,或者定义访问器属性,我们可以使用 Object.defineProperty
var o = { a: 1 };
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
//a和b都是数据属性,但特征值变化了
Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
o.b = 3;
console.log(o.b); // 2
在创建对象时,也可以使用 get 和 set 关键字来创建访问器属性
var o = { get a() { return 1 } };
console.log(o.a); // 1
原型对象
对象是一个属性的索引结构(索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用 key 来查找 value 的字典)。 最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java 等流行的编程语言。这个流派叫做基于类的编程语言。还有一种就是基于原型的编程语言,它们利用原型来描述对象。我们的 JavaScript 就是其中代表。 “基于类”的编程提倡使用一个关注分类和类之间关系开发模型
基于原型的面向对象系统通过“复制”的方式来创建新对象。
原型系统的“复制操作”有两种实现思路:一个是并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用;另一个是切实地复制对象,从此两个对象再无关联。历史上的基于原型语言因此产生了两个流派,显然,JavaScript 显然选择了前一种方式。
JavaScript原型系统可以说相当简单,我可以用两条概括:
如果所有对象都有私有字段[[prototype]],就是对象的原型;读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。 这个模型在 ES 的各个历史版本中并没有很大改变,但从 ES6 以来,JavaScript 提供了一系列内置函数,以便更为直接地访问操纵原型。三个方法分别为:Object.create 根据指定的原型创建新对象,原型可以是 null; Object.getPrototypeOf 获得一个对象的原型; Object.setPrototypeOf 设置一个对象的原型。
var cat = {
say(){
console.log("meow~");
},
jump(){
console.log("jump");
}
}
var tiger = Object.create(cat, {
say:{
writable:true,
configurable:true,
enumerable:true,
value:function(){
console.log("roar!");
}
}
})
var anotherCat = Object.create(cat);
anotherCat.say(); //meow~
var anotherTiger = Object.create(tiger);
anotherTiger.say(); //roar!
JavaScript 中的对象分类我们可以把对象分成几类。
宿主对象(host Objects):由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。(window) 内置对象(Built-in Objects):由 JavaScript 语言提供的对象。固有对象(Intrinsic Objects ):由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
原生对象(Native Objects):可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
普通对象(Ordinary Objects):由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。
函数对象的定义是:具有[[call]]私有字段的对象,构造器对象的定义是:具有私有字段[[construct]]的对象。
Promise里的代码为什么比setTimeout先执行?
当拿到一段 JavaScript 代码时,浏览器或者 Node 环境首先要做的就是;传递给 JavaScript 引擎,并且要求它去执行。
宿主环境当遇到一些事件时,会继续把一段代码传递给 JavaScript 引擎去执行,此外,我们可能还会提供 API 给 JavaScript 引擎,比如 setTimeout 这样的 API,它会允许 JavaScript 在特定的时机执行
一个 JavaScript 引擎会常驻于内存中,它等待着我们(宿主)把 JavaScript 代码或者函数传递给它执行。
在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,宿主环境传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了JavaScript ,在ES5 之后引入了 Promise,这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务
那么采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。
JavaScript 引擎等待宿主环境分配宏观任务,在操作系统中,通常等待的行为都是一个事件循环,所以在 Node 术语中,也会把这个部分称为事件循环。
while(TRUE) {
r = wait();
execute(r);
}
整个循环做的事情基本上就是反复“等待 - 执行”
这里每次的执行过程,其实都是一个宏观任务。我们可以大概理解:宏观任务的队列就相当于事件循环。
在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成, 因此,每个宏观任务中又包含了一个微观任务队列
宏任务处于一个队列中,应当先执行完一个宏任务才会执行下一个宏任务,所以在js脚本中,会先执行非异步代码,再执行微任务代码,最后执行宏任务代码。这时候我们进行到了下一个宏任务中,又按照这个顺序执行。
Promise
它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)
var r = new Promise(function(resolve, reject){
console.log("a");
resolve()
});
setTimeout(()=>console.log("d"), 0)
r.then(() => console.log("c"));
console.log("b")
不论代码顺序如何,d 必定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。