【线上课笔记】Deep Javascript FoundationV3 线上课笔记- 1 【存疑】

150 阅读7分钟

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数字化备份返回。

CleanShot 2022-11-05 at 12.07.38@2x.png

下面是用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上像是一种运算,它不是语言规范中的内容,但是是类型转化的关键。每次类型转化发生时都会进行抽象运算。

  1. ToPrimitive(hint)

背后原理: 假设hint是number(需要转换为number), ToPrimitive算法会先调用valueOf(),如果结果是原始数据,那么算法结束,但如果不是,则继续调用toString(),如果两者调用后都没得到想要的原始数据,那么会报错。 如果hint是string,则valueOf(), toString()的调用顺序与上面相反。

  1. toString()

当我们调用toString()时,实际上是调用了ToPrimitive(hint:string) 。

  1. toNumber() 这里特别提到的几点是
  • “-0”是-0,
  • 开头的空格和0都会被自动去除
  • true和false在这里被转化为1和0(而非NaN)是一个bad idea。
  • undefined变成NaN很好理解, “”和null被转化为0而不是NaN。

CleanShot 2022-11-05 at 16.33.27@2x.png

CleanShot 2022-11-05 at 16.34.58@2x.png

当调用toNumber()时,实际是ToPrimitive(hint:number) ,前面提到过这个操作会先调用valueOf(),对于数组/对象,valueOf()其实return this,也就是原样不动把参数返回,所以实际上调用的是接下来的toString()。或者说: 对于对象来说,toNumber()会先调用toString(),再根据返回值转数字。

以下面的这些数组为例:

CleanShot 2022-11-05 at 16.53.48@2x.png 把一个对象进行toNumber实际上是对它进行toString操作。空数组进行toString后得到的是空字符串,而空字符串会被转化为0,所以空数组会被转化为0,即使是像[[[]]] 这样的数组。 对于第三个和第四个,它们在转化为tostring时,会被变成空字符串,从而再变为0。

对于对象,当对象调用tostring时,会被转化为一串字符,这串字符再被转化为NaN。

  1. toBoolean()

toBoolean方法比较直接,像是查字典一样,它会直接去查哪些值是falsy,哪些值是truthy。 falsy的值比较少:

  • 空字符串
  • 0和-0
  • null
  • NaN
  • false
  • undefined 例如:当传入空数组时,它不在falsy表上,所以会返回true。

数据转换的corner cases

CleanShot 2022-11-05 at 17.38.24@2x.png 留意上面最后一个,当我们用new Boolean后, 实际上创建了一个object,而这样的object是truthy的

这里还提到了为什么Kyle认为把布尔值转换为0和1不是一个好主意。见下图。 CleanShot 2022-11-05 at 17.44.47@2x.png

练习题

这部分的练习题有两道,第一道比较简单,第二道题目要求见下:

CleanShot 2022-11-06 at 11.32.49@2x.png

我的代码

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

CleanShot 2022-11-06 at 11.45.35@2x.png

在==比较的标准中,可以看出它会先确认类型,如果类型相同,会调用===, 所以:

  1. 流行的说法「==只比较值,不确认类型」是不正确的,本质上它们两个都会去检测类型是否相同。
  2. 在类型相同的情况下,使用== 和===没有区别。

继续往下看,其他的要点有:

1.== 倾向于进行数字化比较

(1)如果x和y中一方是number,一方是string,那么会将两者都转换成数字进行比较。 (2)如果一方是boolean,也是转化成数字进行比较;()

  1. 如果有一方是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) 如果不确定类型,说明可能没完全理解代码,如果确实不知道,要让这种不确定性对读者清楚展现出来,此时===可能是个更好的选择。如果到处使用===,仿佛在对读者传达这样的信息:「我不确定数据类型,所以我要用===来保护我的代码」。

CleanShot 2022-11-06 at 12.32.20@2x.png

练习题

要求:

CleanShot 2022-11-06 at 21.16.13@2x.png

我的代码(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;
}

比较:

  1. 自己对Object.is()方法不够清楚,谨慎起见把它放在了最后,导致前面要多加一些判定条件。=> 应该先搞清楚Object.is()的方法,再把Object.is()不能判定的项放在后面。string和number===的情况也应该放在Object.is()部分来做。

查了一下: CleanShot 2022-11-06 at 21.19.04@2x.png 基本上来说,除了Object.is()能判定的,我们需要加的判定条件是: (1)让null和undefined也相等; (2)字符串和数字的匹配

  1. 从整洁度来看,把else if换行写似乎看起来清楚很多..而且要注释掉一整个else if部分时也更方便;

  2. 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%推荐命名的函数表达式写法,理由有三个:

  1. reliable function self-reference(recursion,etc)(这里的意思是函数表达式函数相当于是只读的,但它被赋值的变量不是只读的, 所以引用前者更安全)
  2. more debuggable stack traces
  3. more self-documenting code

Kyle也不推荐箭头函数。

练习