The exclamation mark in front of the array means not. So the line asks whether an empty array equals the opposite of an empty array, and JavaScript says yes. Double equals is like a translator that rewrites both sides as numbers before comparing them, and in translation both sides turn into zero.
The curse
console.log([] == ![]); // prints: true
Something equals its own opposite. To see why, take the line apart one step at a time.
Step by step
Step 1. The not operator runs first. It turns its value into a boolean and flips it. Every array is truthy, even an empty one, so "not an array" is false. The line now reads [] == false.
Step 2. When one side of == is a boolean, JavaScript turns that boolean into a number. false becomes 0. The line now reads [] == 0.
Step 3. When one side is an object (arrays are objects) and the other is a number, the object is turned into a primitive value. An array joins its items into text, and an empty array becomes the empty string "". The line now reads "" == 0.
Step 4. A string compared with a number is turned into a number. "" becomes 0. The line now reads 0 == 0, which is true.
console.log(Boolean([])); // prints: true
console.log(Number(false)); // prints: 0
console.log(Number(String([]))); // prints: 0
console.log("" == 0); // prints: true
This also explains other strange results from the double equals lesson, like [] == false and "" == 0. Both are true for exactly these reasons.
How to break the curse
Use triple equals. === never converts anything. If the two sides are different types, the answer is false straight away.
console.log([] === ![]); // prints: false
console.log("" === 0); // prints: false
An array and a boolean are different types, so there is nothing more to check. No converting, no tricks.
If what you really want to know is whether an array is empty, ask that directly with toys.length === 0. Comparing an array to false or 0 only works by accident.
Bonus twist: [] == [] is false
console.log([] == []); // prints: false
const box = [];
const sameBox = box;
console.log(box === sameBox); // prints: true
When both sides are objects, == and === do the same thing. They check whether both sides are the very same object in memory, as in the copy versus reference lesson. Each [] creates a brand new array, so you get two arrays that look alike, like two identical toys from the same shop. box and sameBox point at one single array, so they are equal.
To compare what is inside two arrays, compare the items. For short lists of simple values, JSON.stringify(a) === JSON.stringify(b) is a quick check.
Rule of thumb
Use ===, because it never converts types, and it works like garlic against JavaScript curses.
Checking whether an array is empty by treating it as a boolean. Arrays are always truthy, so if (toys) runs even for []. Check toys.length === 0 instead.
Check yourself
Every array is truthy, even an empty one, so not-an-array is false. Then == turns false and [] into 0.