Unknown things of javascript, inspired by You-Dont-Know-JS and javascript-questions
BTW, all the following examples are running in non-strict mode.
react
All the following codes can run within https://codesandbox.io/s/new
1 What's the output?
Click Console.log, then Click me. Wait for 3 seconds, what's the output?
import React from "react"; import ReactDOM from "react-dom"; const { useState } = React; function Demo() { const [count, setCount] = useState(0); function log() { setTimeout(() => { console.log("count: " + count); }, 3000); } return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={log}>Console.log</button> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<Demo />, rootElement);
Answer
Why?
Within every rerender, the count is old in setTimeout.
2 What's the output?
Click add, what's the output?
import React from "react"; import ReactDOM from "react-dom"; const { useState, useRef } = React; function Demo(props) { const { list, onChange } = props; const cloneList = [...list]; const cloneListRef = useRef(cloneList); console.log("...render", cloneListRef.current === cloneList); return ( <div> <button onClick={() => onChange([...list, 1])}>add</button> list: {JSON.stringify(list)} </div> ); } function App() { const [list, setList] = useState([]); const handleChange = list => setList(list); return <Demo list={list} onChange={handleChange} />; } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
Answer
...render true // initial render
...render false
Why?
Within every rerender, the cloneList is new.
And only in the first render, the cloneList equals cloneListRef.current.
3 What's the output?
Click post, what's the output?
import React from "react"; const { useState } = React; let id = 0; export default function App() { const [list, onChange] = useState([]); const onStatus = (payload) => { if (payload.status === "posting") { onChange([...list, payload]); } else if (payload.status === "done") { const newlist = list.map((item) => item.id === payload.id ? payload : item ); onChange(newlist); } }; const post = () => { const payload = { id: id }; onStatus({ ...payload, status: "posting" }); id++; setTimeout(() => { onStatus({ ...payload, status: "done" }); }, 3000); }; console.log("list", list); return ( <div> <button onClick={() => post()}>post</button> list: {JSON.stringify(list)} </div> ); }
Answer
list: []
list: [{id: 0, status: 'posting'}]
list: []
Why?
Within else if (payload.status === "done") { const newlist = list.map(item => {, the list is the original value [].
4 What's the output? (use a cloneList base on #3)
Double click the button post quickly
import React from "react"; const { useState } = React; let id = 0; export default function App() { const [list, onChange] = useState([]); // Begin different with previous question const cloneList = [...list]; const onStatus = (payload) => { if (payload.status === "posting") { cloneList.push(payload); onChange(cloneList); } else if (payload.status === "done") { const newlist = cloneList.map((item) => item.id === payload.id ? payload : item ); onChange(newlist); } }; // End different with previous question const post = () => { const payload = { id: id }; onStatus({ ...payload, status: "posting" }); id++; setTimeout(() => { onStatus({ ...payload, status: "done" }); }, 3000); }; console.log("list", list); return ( <div> <button onClick={() => post()}>post</button> list: {JSON.stringify(list)} </div> ); }
Answer
list: []
list: [{id: 0, status: 'posting'}]
list: [{id: 0, status: 'posting'}, {id: 1, status: 'posting'}]
list: [{id: 0, status: 'done'}]
list: [{id: 0, status: 'posting'}, {id: 1, status: 'done'}]
Why?
Within every onStatus, the cloneList is old(closure problem).
How to fix it? Use useRef?
5 What's the output? (useRef base on #4)
Double click the button post quickly
import React from "react"; const { useState, useRef } = React; let id = 0; export default function App() { const [list, onChange] = useState([]); // Begin different with previous question const cloneListRef = useRef(list); const onStatus = payload => { let cloneList = cloneListRef && cloneListRef.current.slice(); // End different with previous question if (payload.status === "posting") { cloneList.push(payload); onChange(cloneList); } else if (payload.status === "done") { const newlist = cloneList.map((item) => item.id === payload.id ? payload : item ); onChange(newlist); } }; const post = () => { const payload = { id: id }; onStatus({ ...payload, status: "posting" }); id++; setTimeout(() => { onStatus({ ...payload, status: "done" }); }, 3000); }; console.log("list", list); return ( <div> <button onClick={() => post()}>post</button> list: {JSON.stringify(list)} </div> ); }
Answer
list: []
list: [{id: 0, status: 'posting'}]
list: [{id: 1, status: 'posting'}]
list: []
list: []
Why?
cloneListRef cached the value of list in first render, and then never change.
6 What's the output? (update the ref value base on #5)
Double click the button post quickly
import React from "react"; const { useState, useRef, useEffect } = React; let id = 0; export default function App() { const [list, onChange] = useState([]); const cloneListRef = useRef(list); // Begin different with previous question useEffect(() => { cloneListRef.current = list; }, [list]); const onStatus = payload => { let cloneList = cloneListRef && cloneListRef.current.slice(); // End different with previous question if (payload.status === "posting") { cloneList.push(payload); onChange(cloneList); } else if (payload.status === "done") { const newlist = cloneList.map((item) => item.id === payload.id ? payload : item ); onChange(newlist); } }; const post = () => { const payload = { id: id }; onStatus({ ...payload, status: "posting" }); id++; setTimeout(() => { onStatus({ ...payload, status: "done" }); }, 3000); }; console.log("list", list); return ( <div> <button onClick={() => post()}>post</button> list: {JSON.stringify(list)} </div> ); }
Answer
list: []
list: [{id: 0, status: 'posting'}]
list: [{id: 0, status: 'posting'}, {id: 1, status: 'posting'}]
list: [{id: 0, status: 'done'}, {id: 1, status: 'posting'}]
list: [{id: 0, status: 'done'}, {id: 1, status: 'done'}]
Why?
Sync the value list to cloneListRef.current immediately, and the onStatus will use the updated value.
6 What's the output?
blur the Input
const { useState } = React; const logValue = e => { console.log("...e", e.target.value); }; const Demo = () => { const handleBlur = e => { setTimeout(() => { logValue(e); }); }; return <input value={23} onBlur={handleBlur} />; }; export default Demo;
Answer
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property `target` on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist(). See https://fb.me/react-event-pooling for more information.
VM33:12 Uncaught TypeError: Cannot read property 'value' of null
Why?
1. js basic types
1.1 What's the output?
a = [1, 2, 3, 4]; delete a[1]; console.log(a.length);
Answer
Output:
Why?
- After
delete a[1], a becomes[1, empty, 3, 4]
1.2 What's the output?
let list = [1, 2, 3, 4]; let alreadyList = [2, 3]; let cloneList = [...list]; for (let i = 0; i < list.length - 1; i++) { let item = list[i]; if (alreadyList.includes(item)) { cloneList.splice(i, 1); } } console.log("...", cloneList);
Answer
Output:
Why?
- After
delete 2 - cloneList[1], cloneList becomes[1, 3, 4] - When
delete 3 - cloneList[2], cloneList becomes[1, 3]
1.3 What's the output?
console.log(42.toFixed(3));
Answer
Output:
Uncaught SyntaxError: Invalid or unexpected token
Why?
Within 42.toFixed(3), the . will be regarded as a part of number, so (42.)toFixed(3) throws error.
// Correct:
- (42).toFixed(3); // "42.000"
- num = 42; num.toFixed(3); // "42.000"
- 42..toFixed(3); // "42.000"
1.4 What's the output?
console.log(0.1 + 0.2 === 0.3);
Answer
Output:
Why?
For lauguage following IEEE 754 rule such as javascript, 0.1 + 0.2 outputs 0.30000000000000004.
A safe way to comprare values:
function numbersCloseEnoughToEqual(n1, n2) { return Math.abs(n1 - n2) < Number.EPSILON; // Number.EPSILON: 2.220446049250313e-16 } let a = 0.1 + 0.2; let b = 0.3; numbersCloseEnoughToEqual(a, b);
1.5 What's the output?
a = "12" + 9; console.log(a, typeof a); b = "12" - 9; console.log(b, typeof b);
Answer
Output:
Why?
string + numberwill transform number to string, outputs stringstring - numberwill transform string to number, outputs number
1.6 What's the output?
Run seperately:
JSON.stringify(undefined); JSON.stringify(function() {}); JSON.stringify([1, undefined, function() {}, 4, new Date()]); JSON.stringify({ a: 2, b: function() {}, c: Symbol.for("ccc"), d: 1, e: undefined });
Answer
Output:
undefined undefined [1,null,null,4,"2019-08-14T01:52:25.428Z"] {"a":2,"d":1}
Why?
JSON.stringify will ignore undefined, function, symbol
1.7 What's the output?
a = Array(3); b = new Array(3); c = Array.apply(null, { length: 3 }); d = [undefined, undefined, undefined]; console.log( a.map(function(v, i) { return i; }) ); console.log( b.map(function(v, i) { return i; }) ); console.log( c.map(function(v, i) { return i; }) ); console.log( d.map(function(v, i) { return i; }) );
Answer
Output:
Different browsers may behave differently, while within current Chrome, the output is:
[empty × 3] [empty × 3] [0, 1, 2] [0, 1, 2]
Why?
Array(num)is as same asnew Array(num), since the browser will auto addnewin before ofArray(num)new Array(3)create a array, in which every member isemptyunit (undefinedtype).a.map(..)&b.map(..)will be failed, as the array is full ofempty,mapwill not iterate them.
1.8 What's the output?
x = [1, 2, { a: 1 }]; y = x; z = [...x]; y[0] = 2; (y[2].b = 2), (z[2].a = 4); console.log(x, y, z);
Answer
Output:
[2, 2, { a: 4, b: 2 }][(2, 2, { a: 4, b: 2 })][(1, 2, { a: 4, b: 2 })];
Why?
z = [...x]is shallow copy
1.9 What's the output?
a = new Array(3); b = [undefined, undefined, undefined]; console.log(a.join("-")); console.log(b.join("-"));
Answer
Output:
Different browsers may behave differently, while within current Chrome, the output is:
Why?
join works differently with map:
function fakeJoin(arr, connector) { var str = ""; for (var i = 0; i < arr.length; i++) { if (i > 0) { str += connector; } if (arr[i] !== undefined) { str += arr[i]; } } return str; } var a = new Array(3); fakeJoin(a, "-"); // "--"
2. this
2.1 What's the output?
obj = { a: 1, getA() { console.log("getA: ", this.a); } }; obj.getA(); x = obj.getA; x(); setTimeout(obj.getA, 100);
Answer
Output:
getA: 1 getA: undefined (a timerId number) getA: undefined
Why:
- It's
Implicitly Lost - Even though getA appears to be a reference to obj.getA, in fact, it's really just another reference to getA itself. Moreover, the call-site is what matters, and the call-site is getA(), which is a plain, un-decorated call and thus the
default bindingapplies. default bindingmakesthisthe global(Window) or undefined (depends on if this isstrict mode).
Question:How to change x(); setTimeout(obj.getA, 100);, make it output getA: 1.
2.2 What's the output?
obj = { a: 1, getA: () => { console.log("getA: ", this.a); } }; setTimeout(obj.getA.bind(obj), 100);
Answer
Output: getA: undefined.
Arrow functions can never have their own this bound. Instead, they always delegate to the lexical scope (Window).
2.3 What's the output?
function foo() { let a = 2; this.bar(); } function bar() { console.log(this.a); } foo();
Answer
Output:
Why?
- Every time you feel yourself trying to mix lexical scope look-ups with this, remind yourself: there is no bridge.
2.4 What's the output?
let boss1 = { name: "boss1" }; let boss2 = { name: "boss2" }; let boss1returnThis = function() { return this.name; }.bind(boss1); console.log(boss1returnThis.bind(boss2)()); console.log(boss1returnThis.apply(boss2)); console.log(boss1returnThis.call(boss2));
Answer
Output:
Why?
- For binded this, it cannot be reassigned, even with .bind(), .apply() or .call()
2.5 What's the output?
let boss1 = { name: "boss1" }; let boss2 = { name: "boss2" }; // Begin pay attention let boss1returnThis = (() => { return this; }).bind(boss1); // End pay attention console.log(boss1returnThis.bind(boss2)()); console.log(boss1returnThis.apply(boss2)); console.log(boss1returnThis.call(boss2));
Answer
Output:
Why?
- Arrow functions can never have their own this bound. Instead, they always delegate to the lexical scope (Window).
- For arrow functions, this can't be reassigned, even with .bind(), .apply() or .call()
2.6 What's the output?
var value = 1; var foo = { value: 2, bar: function() { return this.value; } }; console.log(foo.bar()); console.log((foo.bar = foo.bar)()); console.log((false || foo.bar)()); console.log((foo.bar, foo.bar)());
Answer
Output:
Why?
- Last 3 console.log do apply GetValue to the result of evaluating Expression.
- GetValue(lref) changes
thisto be global(window).
2.7 What's the output?
// Begin pay attention let value = 1; let foo = { value: 2, bar: function() { return this.value; } }; // End pay attention console.log(foo.bar()); console.log((foo.bar = foo.bar)()); console.log((false || foo.bar)()); console.log((foo.bar, foo.bar)());
Answer
Output:
2; undefined; undefined; undefined;
Why?
letis not global whilevaris.
So the following code will output 1 undefined 2
let a = 1; var b = 2; console.log(a, window.a, window.b);
3. property
3.1 What's the output?
x = Symbol("x"); a = [2, 3, 4, 5, 6, 7]; a.b = 1; a[x] = 0; for (let key in a) { console.log(key); }
Answer
Output:
Why?
for ... inloop will iterates all enumerable, non-Symbol properties.
3.2 What's the output?
x = Symbol("x"); a = [2, 3, 4, 5, 6, 7]; a.b = 1; a[x] = 0; for (let val of a) { console.log(val); }
Answer
Output:
Why?
- The for...in statement iterates over the enumerable, non-Symbol properties of an object, in an arbitrary order.
- The for...of statement iterates over values that the iterable object defines to be iterated over.
3.3 What's the output?
class A { x = 1; getX() { return this.x; } } a = new A(); b = Object.assign({}, a); c = { ...a }; console.log(b, c, "getX" in b, "getX" in c);
Answer
Output:
`{x: 1} {x: 1} false false`;
Why?
Object.assign&...&...in...only iterates enumerable, non-Symbol properties of the given object directly, excluding the properties ofx.__proto__,getterandsetter.
3.4 What's the output?
obj = { a: 1 }; x = Object.create(obj); Object.defineProperty(x, "b", { value: 2, enumerable: false }); x.c = 3; for (let k in x) { console.log("key: " + k); } console.log(Object.getOwnPropertyNames(x)); console.log(Object.keys(x)); console.log(Object.assign({}, x)); JSON.stringify(x); console.log(x.hasOwnProperty("a"), x.hasOwnProperty("c")); console.log("a" in x, "c" in x);
Answer
Output:
key: c; key: a; ["b", "c"]; ["c"] {c: 3} "{"c":3}" false true true true
Why?
x = Object.create(obj)creates a new object, using the existing objectobjas the prototype of the newly created objectx.
Remember the keywords:
for...in: excludingnon-enumerable, including__proto__Object.getOwnPropertyNames&hasOwnProperty: includingnon-enumerable, excluding__proto__Object.keys&Object.assign&JSON.stringify: excludingnon-enumerable&__proto__... in ...: includingnon-enumerable&__proto__
3.5 What's the output?
a = { x: 2 }; b = Object.create(a); console.log(b.hasOwnProperty("x")); b.x++; console.log(b.hasOwnProperty("x"));
Answer
Output:
Why?
- Object.create creates a new object, using the existing object as the prototype of the newly created object.
b.x++will runb.x = b.x + 1, which will add own propertyxforb.
4. __proto__ && prototype
4.1 What's the output?
function A(name) { this.name = name; } A.prototype.myName = function() { return this.name; }; function B(name, label) { A.call(this, name); this.label = label; } function C(name, label) { A.call(this, name); this.label = label; } B.prototype = A.prototype; C.prototype = new A(); B.prototype.myName = function() { return 111; }; x = new A("xxx"); y = new B("yyy"); z = new C("zzz"); console.log(x.myName(), y.myName(), z.myName());
Answer
Output:
Why?
B.prototype = A.prototypeis assign the reference of objectA.prototypetoB.prototype, soB.prototype.myName=....changesA.prototype.myName.new A()returns{name: undefined},C.prototype = new A()meansC.prototype = {name: undefined}.- Since
z.__proto__( ===C.prototype) has nomyName, soz.myNamewill bez.__proto__.__proto__.myName( ===C.prototype.__proto__.myName) - Since
C.prototype.__proto__ === A.prototype, soC.prototype.__proto__.myNamewill beA.prototype.myName, which has changed byB.prototype.myName=.....
So how to make A.prototype.myName unchanged when setting B.prototype.myName=....?
Fix B.prototype = A.prototype by B.prototype = Object.create(A.prototype)
4.2 What's the output?
class C { constructor() { this.num = Math.random(); } } c1 = new C(); C.prototype.rand = function() { console.log("Random: " + Math.round(this.num * 1000)); }; c1.rand();
Answer
Output:
Random: 890; // a random number between 0~1000
Why?
classin js is made with [[Prototype]], soc1.__proto__===C.prototype
4.3 What's the output?
function Test(oo) { function F() {} F.prototype = oo; return new F(); } o = { x: 1, getX: function() { return 111; } }; p = Test(o); q = Object.create(o); console.log(p); console.log(q); console.log(p.__proto__ === q.__proto__);
Answer
Output:
Why?
p.__proto__equals(new F()).__proto__equalsF.prototypeequalsoq = Object.create(o)makesq.__proto__equalsoTestis polyfill ofObject.createfor browsers which doesn't support es5. reference- So, don't mock class by setting
__proto__/prototype/new, just useObject.create:
let Widget = { init: function(width, height) { this.width = width || 50; } }; let Button = Object.create(Widget); Button.setup = function(width, height, label) { this.init(width, height); this.label = label || "Default"; };
4.4 What's the output?
function Animal(name) { this.name = name || "Animal"; } function Cat() {} Cat.prototype = new Animal(); Cat.prototype.name = "Cat"; function Dog() {} Dog.prototype = Object.create(Animal.prototype); cat = new Cat(); dog = new Dog(); Animal.prototype.eat = function(food) { console.log(this.name + " is eating " + food); }; console.log(cat.eat("fish")); console.log(dog.eat("rice"));
Answer
Output:
cat is eating fish undefined is eating rice
Why?
cat.__proto__.__proto__equals(Cat.prototype).__proto__equalsAnimal.prototypecat.eat('fish')will callcat.__proto__.__proto__.eat('fish')dog.__proto__equalsDog.prototypeequalsAnimal.prototypedog.eat("rice")will calldog.__proto__.eat('rice')
It means that properties of Animal.prototype will be shared by all instances, including those inherited earlier.
5. setTimeout & ……
5.1 What's the output?
setImmediate(function(){ console.log(1); },0); setTimeout(function(){ console.log(2); },0); new Promise(function(resolve){ console.log(3); resolve(); console.log(4); }).then(function(){ console.log(5); }); console.log(6); console.log(8);
Answer
Output:
3 4 6 8 5 2 1 // (理论上开始 setTimeout 和 setImmediate 顺序不稳定,但我 Node v16.14.0 测下来多次都是如此 )
Why?
- macrotasks: setTimeout,setInterval, setImmediate,requestAnimationFrame,I/O,UI渲染
- microtasks: Promise, process.nextTick, Object.observe, MutationObserver
当一个程序有:setTimeout, setInterval ,setImmediate, I/O, UI渲染,Promise ,process.nextTick,Object.observe, MutationObserver的时候:
- 先执行 macrotasks 中的 I/O -》 UI渲染-》requestAnimationFrame
- 再执行 microtasks :process.nextTick -》 Promise -》MutationObserver ->Object.observe
- 再把 macrotasks 中 setTimeout setInterval setImmediate【三个货不讨喜】 塞入一个新的 macrotasks.
Reference: Vue 中如何使用 MutationObserver 做批量处理?