Unknown things of javascript, inspired by You-Dont-Know-JS and javascript-questions
BTW, all the following examples are running in non-strict mode.
All the following codes can run within https://codesandbox.io/s/new
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
count: 0
Why?
Within every rerender, the count is old in setTimeout.
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.
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 [].
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?
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.
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.
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?
a = [1, 2, 3, 4];
delete a[1];
console.log(a.length);Answer
Output:
4;Why?
- After
delete a[1], a becomes[1, empty, 3, 4]
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:
[1, 3];Why?
- After
delete 2 - cloneList[1], cloneList becomes[1, 3, 4] - When
delete 3 - cloneList[2], cloneList becomes[1, 3]
console.log(42.toFixed(3));Answer
Output:
Uncaught SyntaxError: Invalid or unexpected tokenWhy?
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"
console.log(0.1 + 0.2 === 0.3);Answer
Output:
false;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);a = "12" + 9;
console.log(a, typeof a);
b = "12" - 9;
console.log(b, typeof b);Answer
Output:
129 string
3 "number"Why?
string + numberwill transform number to string, outputs stringstring - numberwill transform string to number, outputs number
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
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.
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
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, "-"); // "--"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: undefinedWhy:
- 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.
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).
function foo() {
let a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();Answer
Output:
undefined;Why?
- Every time you feel yourself trying to mix lexical scope look-ups with this, remind yourself: there is no bridge.
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:
boss1;
boss1;
boss1;Why?
- For binded this, it cannot be reassigned, even with .bind(), .apply() or .call()
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:
Window;
Window;
Window;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()
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:
2;
1;
1;
1;Why?
- Last 3 console.log do apply GetValue to the result of evaluating Expression.
- GetValue(lref) changes
thisto be global(window).
// 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);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:
0;
1;
2;
3;
4;
5;
b;Why?
for ... inloop will iterates all enumerable, non-Symbol properties.
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:
2;
3;
4;
5;
6;
7;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.
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.
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 trueWhy?
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__
a = { x: 2 };
b = Object.create(a);
console.log(b.hasOwnProperty("x"));
b.x++;
console.log(b.hasOwnProperty("x"));Answer
Output:
false;
true;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.
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:
111 111 111Why?
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)
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~1000Why?
classin js is made with [[Prototype]], soc1.__proto__===C.prototype
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:
F {}
{}
trueWhy?
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";
};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 riceWhy?
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.
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 做批量处理?