introduction部分
video1总结
vidoe1中Kyle主要提到以下几个要点:
- 我们在编写代码时会有一个mental model,也就是对代码执行结果的预期,如果不符合预期,也许一些人会认为是js语言设计缺陷。他说根据自己的经历,很多JAVA/c开发者都会去阅读官方标准,但很多前端开发者却并不会。
- 当我们出现实际结果与预期不一致时,应该先问:“specification说了什么?”如果实际结果与specification不一致,说明代码有bug,而如果一致,则说明是我们的大脑有bug---incorect thinking(错误的思维)(很喜欢这个大脑有bug的概念...);
video1 中的代码示例是++运算符,Kyle提出一个疑问是:如果++运算符和一个字符串一起用会发生什么?我们通常认为:++放在后面时,是“先用再加”,那么字面上这意味着:我们是先原封不动地把变量a拿过来,然后再进行+1操作,但事实上我们发现:js并不是直接原封不动地把a拿过来,而是先将其转换成数字5。
let a = "5";
console.log(a++); //5
console.log(typeof a);//number
console.log(a);//6
video2总结
这部分Kyle解释了一下前面后左++运算符的原理,根据标准文档,左++先将x转换为数字并存储为另一个变量(相当于是已经被数字化的x备份),然后对原来的x进行加1操作,最后把原来的x数字化备份返回。
下面是用js函数模拟了一下过程。
function plusPlus(orig_x) {
let orig_x_coerced = Number(orig_x);
x = orig_x_coerced + 1;
return orig_x_coerced;
}
以上开始看出,严格来说先用后加的说法是不准确的,它并不是使用完x再进行加的操作,而是先存储一个数字化x备份,然后对数字化的x进行加1操作,最后把备份档返回。
1. types部分
video1 primitive types 总结
Kyle先说了一个广为流传的误解:
错误:Javascript一切都是对象
之所以存在这个说法,是因为Javascript中的数据类型运作起来都像是「对象」一样。
另外提到的一句有用信息是:
In Javascript, variables don't have types, values do. 变量没有类型,是变量的值有类型。
video2 总结
关于typeof
- typepf运算符返回结果永远是string,它的return像是enum类型数据。
- 对function进行typeof后,返回值是function,但对array进行typeof后,返回值却是object。array和function都是object的子类型,理论上要么都返回大类型,要么都返回子类型,所以这里的行为是不合逻辑的,这是一个历史遗留问题。对null理论上也应该返回null才更有用,Kyle说这是一个bug。
console.log(typeof a); // “undefined”
// 变量a并没有被声明,理论上可以加一个类型“undeclared”
let b = null;
console.log(typeof b); // “object”
let c = function() {};
console.log(typeof c); // “function“
let d = [1, 2, 3];
console.log(typeof d); // ”object“
//...未来可能会有
let v = 42n;
// or: BigInt(42);
typeof v; //"bigint"
关于NaN
- NaN 是js中唯一一不具有自等性的值。(NaN !== NaN)
- ES6新增的Number.isNaN()在进行NaN判断前并不会先将变量转换为数字。
const str = "I am a string";
console.log(isNaN(str)); // true
console.log(Number.isNaN(str)); //false
- NaN虽然字面是not a number,但本质上是“is an invalid number”,所以它的typeof结果是number
关于负0
let negZero = -0;
console.log(negZero === -0); // true
console.log(negZero === 0); //true 和0相等
console.log(negZero > 0);
console.log(negZero < 0);
console.log(negZero.toString()); // 0 变成字符串后为0
//ES6新增
console.log(Object.is(negZero, 0)); //false
console.log(Object.is(negZero, -0));//true
0-的应用:Kyle说之前他做一个关于地图和车的项目,当车停止的时候,需要能表示出车在哪个方向运行时停止。又比如,比如物价回到0基准线,它是跌到0的、还是涨到0的。此时正负号则可以用来帮助表示方向。
js里有Math.sign可以用来判断数字的正负,但是当参数为-0时,结果变得很奇怪,所以可以考虑使用修正后的自定义sign函数。
console.log(Math.sign(-3)); //-1
console.log(Math.sign(3)); //1
console.log(Math.sign(-0)); //-0
console.log(Math.sign(0)); //0
function sign(num) {
return num !== 0 ? Math.sign(num) : Object.is(num, -0) ? -1 : 1;
}
console.log(sign(-3)); //-1
console.log(sign(3)); //1
console.log(sign(-0)); //-1
console.log(sign(0)); //1
types部分练习
这部分练习是写一个Object.is(..) polyfill, 重点是写出对于NaN和-0的判断,因为其他值用===即可判断,但当参数是NaN,NaN用===不和自身相等,而-0===0。
练习要求是:尽量不要用Number.isNaN(..)直接判断,而是想想看有没有什么其他方法。判断-0时也尽量不要用内置的工具方法,这点给出了提示:考虑如何利用-Infinity。
我的答案:
if (!Object.is || true) {
Object.is = function ObjectIs(v, ref) {
//思路:NaN首先是数字类型,它和其他数字有一个很明显的不同是:
// 它转化成字符串后在值上不和自身相等。
function my_isNaN(value) {
if (typeof(value) === 'number') {
const value_copy = value;
return value_copy != value.toString();
} else {
return false;
}
}
// 思路:1除以-0结果是-Infinity
function my_isNegZero(value) {
return 1 / value === -Infinity;
}
//check NaN 如果有一方是NaN,则判断是否两个都是NaN
if (my_isNaN(v) || my_isNaN(ref)) {
return my_isNaN(v) === my_isNaN(ref);
}
//check negative zero 同上NaN的判定思路
if (my_isNegZero(v) || my_isNegZero(ref)) {
return my_isNegZero(v) === my_isNegZero(ref);
}
//others
return v === ref;
}
}
老师的版本和我的代码中的问题
if (!Object.is /*|| true*/) {
Object.is = function ObjectIs(x,y) {
var xNegZero = isItNegZero(x);
var yNegZero = isItNegZero(y);
if (xNegZero || yNegZero) {
return xNegZero && yNegZero;
}
else if (isItNaN(x) && isItNaN(y)) {
return true;
}
else if (x === y) {
return true;
}
return false;
// **********
function isItNegZero(x) {
//~~这里需要先写x === 0是因为1/-Infinity结果也是-Infinity。
return x === 0 && (1 / x) === -Infinity;
}
function isItNaN(x) {
// ~~直接这样判断即可,因为NaN是唯一不和其自身相等的值。我的方法反而变复杂了。
return x !== x;
}
};
}
除了逻辑上的问题外,需要学习的就是代码的漂亮清晰程度(例如工具类的定义位置,xNegZero这些变量的定义,if else的清晰结构)
2. coercion(类型转换)部分
抽象运算
抽象运算在concept上像是一种运算,它不是语言规范中的内容,但是是类型转化的关键。每次类型转化发生时都会进行抽象运算。
- ToPrimitive(hint)
背后原理: 假设hint是number(需要转换为number), ToPrimitive算法会先调用valueOf(),如果结果是原始数据,那么算法结束,但如果不是,则继续调用toString(),如果两者调用后都没得到想要的原始数据,那么会报错。 如果hint是string,则valueOf(), toString()的调用顺序与上面相反。
- toString()
当我们调用toString()时,实际上是调用了ToPrimitive(hint:string) 。
- toNumber() 这里特别提到的几点是
- “-0”是-0,
- 开头的空格和0都会被自动去除
- true和false在这里被转化为1和0(而非NaN)是一个bad idea。
- undefined变成NaN很好理解, “”和null被转化为0而不是NaN。
当调用toNumber()时,实际是ToPrimitive(hint:number) ,前面提到过这个操作会先调用valueOf(),对于数组/对象,valueOf()其实return this,也就是原样不动把参数返回,所以实际上调用的是接下来的toString()。或者说: 对于对象来说,toNumber()会先调用toString(),再根据返回值转数字。
以下面的这些数组为例:
把一个对象进行toNumber实际上是对它进行toString操作。空数组进行toString后得到的是空字符串,而空字符串会被转化为0,所以空数组会被转化为0,即使是像[[[]]]
这样的数组。
对于第三个和第四个,它们在转化为tostring时,会被变成空字符串,从而再变为0。
对于对象,当对象调用tostring时,会被转化为一串字符,这串字符再被转化为NaN。
- toBoolean()
toBoolean方法比较直接,像是查字典一样,它会直接去查哪些值是falsy,哪些值是truthy。 falsy的值比较少:
- 空字符串
- 0和-0
- null
- NaN
- false
- undefined 例如:当传入空数组时,它不在falsy表上,所以会返回true。
数据转换的corner cases
留意上面最后一个,当我们用new Boolean后, 实际上创建了一个object,而这样的object是truthy的
这里还提到了为什么Kyle认为把布尔值转换为0和1不是一个好主意。见下图。
练习题
这部分的练习题有两道,第一道比较简单,第二道题目要求见下:
我的代码
function hoursAttended(attended, length) {
if (isValidNumber(attended) && isValidNumber(length)) {
return Number(attended) <= Number(length);
}
return false;
}
// ####################
function isValidNumber(n) {
if (typeof n != 'number' && typeof n != 'string') return false;
const num = Number(n);
if (typeof n == 'string' && (isNaN(num) || n.trim() == '')) {
return false
}
return num >= 0 && parseInt(num) === num
}
范例代码
function hoursAttended(attended,length) {
if (
typeof attended == "string" &&
attended.trim() != ""
) {
attended = Number(attended);
}
if (
typeof length == "string" &&
length.trim() != ""
) {
length = Number(length);
}
if (
typeof attended == "number" &&
typeof length == "number" &&
attended <= length &&
attended >= 0 &&
length >= 0 &&
Number.isInteger(attended) &&
Number.isInteger(length)
) {
return true;
}
return false;
}
3. equality
在==比较的标准中,可以看出它会先确认类型,如果类型相同,会调用===, 所以:
- 流行的说法「==只比较值,不确认类型」是不正确的,本质上它们两个都会去检测类型是否相同。
- 在类型相同的情况下,使用== 和===没有区别。
继续往下看,其他的要点有:
1.== 倾向于进行数字化比较:
(1)如果x和y中一方是number,一方是string,那么会将两者都转换成数字进行比较。 (2)如果一方是boolean,也是转化成数字进行比较;()
- 如果有一方是object,一方是primitive,那么会将obj进行toPrimitive操作。也就是说:==本质上只进行原始类型的比较,如果传入的数据不是原始类型,会被转为原始类型。9执行完毕后会再次重复执行上面的,直到得到比较结果。
例如:
let a = 42;
let b = [42];
console.log(a == b); // true b调用toPrimitive之后会变成字符串“42”。
下面是一个看起来更奇怪的例子。
console.log([] == ![]); // true ==== (1)
console.log([] != []); // true1 =====(2)
// 比较原理:
(1)因为有!符号,所以先将[]转化为true,再把![]转化为false, 根据spec,一方是boolean的话,会进行数字化比较,左边先被转化为primitive “”,再被转化为数字0, 右边的false也是0,所以二者相等。
(2)第二个相当于是!([] == []), 因为两个数据同类型,所以会进行===比较,结果是false,!false则为true。
再看一个例子
let arr = [];
if([]){
// Yes
}
if(arr == true){
// Nope 此时会进行数值比较,左边是0, 右边是1
}
if(arr ==false){
// Yep
}
Nerver use the == when you don't know the types, ever.
类型已知的情况下用== 好于用===:
(1) 当已知类型相同的情况下,用==等同于用===
(2) 当已知类型不同的情况下,用===变得无意义,也会分散读者注意力。
(3) 如果不确定类型,说明可能没完全理解代码,如果确实不知道,要让这种不确定性对读者清楚展现出来,此时===可能是个更好的选择。如果到处使用===,仿佛在对读者传达这样的信息:「我不确定数据类型,所以我要用===来保护我的代码」。
练习题
要求:
我的代码(corner case很多,感觉没整理好思路)
const findAll = (value, arr) => {
const res = [];
const type = typeof value;
for (let i = 0; i < arr.length; i++) {
let cur = arr[i];
if (value == undefined && cur == undefined) {
res.push(cur);
} else if (type == 'boolean') {
if (cur === value) {
res.push(cur);
}
} else if (type == 'string' && !Object.is(cur, -0)) {
if (cur === value || (typeof cur == 'number' && value.trim() != '' && cur == value)) {
res.push(cur);
}
} else if (type == 'number' && !isNaN(value) && !Object.is(value, -0) && !Object.is(cur, -0)) {
if (cur === value || (typeof cur == 'string' && cur.trim() != '' && cur == value)) {
res.push(cur);
}
} else if (Object.is(value, cur)) {
res.push(cur);
}
}
return res;
}
范例代码:
function findAll(match,arr) {
var ret = [];
for (let v of arr) {
if (Object.is(match,v)) {
ret.push(v);
}
else if (match == null && v == null) {
ret.push(v);
}
else if (typeof match == "boolean") { // 疑问:这部分难道不是不需要吗?
if (match === v) {
ret.push(v);
}
}
else if (typeof match == "string" && match.trim() != "" && typeof v == "number" && !Object.is(-0,v)) {
if (match == v) {
ret.push(v);
}
}
else if (typeof match == "number" && !Object.is(match,-0) && !Object.is(match,NaN) && !Object.is(match,Infinity) && !Object.is(match,-Infinity) && typeof v == "string" && v.trim() != "") {
if (match == v) { // 疑问:上面的!Object.is(match,NaN)必要吗?如果match是NaN,下面的if也不会满足。
ret.push(v);
}
}
}
return ret;
}
比较:
- 自己对Object.is()方法不够清楚,谨慎起见把它放在了最后,导致前面要多加一些判定条件。=> 应该先搞清楚Object.is()的方法,再把Object.is()不能判定的项放在后面。string和number===的情况也应该放在Object.is()部分来做。
查了一下:
基本上来说,除了Object.is()能判定的,我们需要加的判定条件是:
(1)让null和undefined也相等;
(2)字符串和数字的匹配
-
从整洁度来看,把else if换行写似乎看起来清楚很多..而且要注释掉一整个else if部分时也更方便;
-
number部分条件中比我多了
!Object.is(match,Infinity) && !Object.is(match,-Infinity),应该是因为想避免数字的Infinity和字符串的Infinity的匹配。
4. scope
这里讲解scope用了一个桶的比喻,全局是一个大桶,每个变量声明相当于是一颗鹅卵石,而我们要把鹅卵石投入到正确的桶中,全局中的函数中如果有变量声明,相当于是在大桶里又放了一个小桶,然后把鹅卵石投进去。 还用了一个比喻是电梯,假设你在一个建筑的1楼(current scope)找东西没找到,那么就搭乘电梯去更高层(更外层的scope)去找。
auto-global
看下面的例子: 当发现topic时,我们的scope manager会先到函数桶里找,发现没有关于topic的声明,然后再到全局的桶里去找,这时也没找到,此时它不会告诉我们它找不到,而是会自动为我们创建一个全局的topic鹅卵石(非严格模式下),在严格模式下则会出现Reference Error: topic is not define。
topic和teacher的区别是,teacher是compile time时创建的,而topic是run time时创建的。会有一些诸如性能等方面的差异。
let teacher = "Kyle";
function otherClass() {
teacher = "Suzy";
topic = "React"; //自动被声明为全局变量
console.log("Welcome!");
}
otherClass();
console.log(teacher); // "Suzy"
console.log(topic); // "React"
function expression
函数声明和函数表达式的一个区别是,后者的函数名不会被放到全局变量中,而是在自己的作用域中(最后一行)。而且anotherTeacher变成了只读对象,无法再被重新赋值。
function teacher() {
/* ..*/
}
const myTeacher = function anotherTeacher() {
anotherTeacher = 5;// 即使在这里重新赋值,下一行结果仍是//[Function: anotherTeacher]
console.log(anotherTeacher);
}
console.log(teacher); // [Function: teacher]
console.log(myTeacher); //[Function: anotherTeacher]
console.log(anotherTeacher); // ReferenceError: anotherTeacher is not defined
myTeacher(); //[Function: anotherTeacher]
Kyle说100%推荐命名的函数表达式写法,理由有三个:
- reliable function self-reference(recursion,etc)(这里的意思是函数表达式函数相当于是只读的,但它被赋值的变量不是只读的, 所以引用前者更安全)
- more debuggable stack traces
- more self-documenting code
Kyle也不推荐箭头函数。