getify / You-Dont-Know-JS

A book series on JavaScript. @YDKJS on twitter.
Other
179.71k stars 33.51k forks source link

"types & grammar": review - jesseharlin #178

Closed the-simian closed 10 years ago

the-simian commented 10 years ago

My review of types and grammar.

Edit: I'm coming back for a second pass on these first three for proofing, etc.

getify commented 10 years ago

FYI: chapter 4 still in flux. I'd focus on 1-3 for now. 4 should be done in the next couple of days. Thanks! :)

the-simian commented 10 years ago

Ch 1 types

There is a lot of really really good stuff in this chapter I agree with very strongly, and think is valuable information:


I only had one thing I think needs more attention:

I should add this is a fairly minor thing, but kind of requires a long explanation to express why I feel as I do, so please don't mistake a wall of text for a fiercely antagonistic disposition to this point, I am merely trying to express what is a kind of a nuanced point. I also posses apparently a minority opinion, so I'm probably wrong but here it goes:

The only one thing in this section I disagree with: typeof null being "object" is a "bug".

Kyle immediately points out correctly that null is the only way to essentially have a falsy object. This is the backbone of my opinion.

As I see it, every primitive type has both truthy and falsy values. The exception is undefined, and for good reason. It is lack of intent; nothing has been assigned. Truthiness does not work conceptually with this type in any way.

Type Falsy Value Truthy Value
undefined undefined makes no conceptual sense
boolean false true
string '' 'anything'
number 0, NaN 1, Infinity
object null {}

So firstly we can see that things besides actual numerical values, like 1 and 2 can be a number (NaN, Infinity). So null being a typeof object, doesn't break suit from this pattern, only reinforces that values usually conform to the existing types. (there is no typeof Infinity = "infinity")

Secondly, the reasoning, as I understand is that null, in contrast to undefined was, and still is used when some value has been set, intentionally to a 'non value' as opposed to haven't been assigned yet. Often times, an empty reference to an object. So this is the intentional absence of any object value, and without this being typeof object, this expressiveness wouldn't exist.

Consider:

[{}.{},{}, null,{}]

and

[{},{},{}, undefined, {}]

In these situations one needs to express a list of objects, and one is falsy - a 'non' value, but is intentionally assigned as such. one might say 'simply use false'. To use false would imply heterogeneous typing, and is subtly different from null. Or fine, use null, but it has a different type ( a type of "null"). Again, we want a falsy object, a list of all items are the same type, and can be treated as such, processed as such by functions/ logic in our code. Something of a different type might intentionally throw an error. A 'falsy object' might have meaning, such as a point in a step when someone explicitly did not select something, but the selection process did occur. (before the selection process it would be undefined). This would be like '' be the same as leaving a text box blank, or NaN the explicit result of parsing something besides a numerical value like 1,2,3,4.

To be more clear consider these:

[1,2, 3,NaN, 5] //for numbers

['a', 'b', 'c', '', 'e'] //for strings

[true, true, true, false, true] //bools

The examples above express a homogenous list of types and an intentional falsy value, as contrast with:

[1,2, undefined, 4] //missing value, was never assigned.

Aside from practice, my worldview about the null value's typeof is also informed by this:

4.3.11 null value primitive value that represents the intentional absence of any object value

and

4.3.9 undefined value primitive value used when a variable has not been assigned a value

That is from the Ecma-262 spec.

I should add that Kyle stating that 'its a bug' is not uncommon , if anything I am taking the minority position with my interpretation of the spec, and when its appropriate to use null vs undefined. Most folks seem to repeat that "its just a bug, forget it, even Brendan Eich says so". (Usually this is followed by a link to Axel Rauchmayer's blog/ famous stack overflow question, or MDN (which just points right back to Axel's blog... again)).

Actually one of the oldest sources of this is On Crockfords site, where He also points out typeof Array being 'object' is also wrong. (but Array is an object- its not like a primitive!); Its down at "Mistakes were made". He even has "capital" String as typeof "string". The primitive (lowercase) string is... but (capital) String() is typeof "object". Basically this table doesn't really differentiate Natives/Constructors from primitives.

So? Very Smart People™ disagree. Maybe I am in the wrong, but based on what I've presented above, this seems logical to me, and consistent with the rest of the language. Even if I am super duper wrong, this is unclear enough it could probably be explained more.

To be fair, there stuff like this further down the spec:

Semantics The value of the null literal null is the sole value of the Null type, namely null. and

4.3.12 Null type type whose sole value is the null value

Which doesn't at all make things more clear. In one place it says null type/value is null, and the other its an intentional absence of an object value (which makes it more like NaN). Whats right? Well I do know null is actually a primitive in how it is implemented in the language, but is returning "object" on typeof really wrong?

Based on my previous points, I feel like going with the first description seems more logical. Making the typeof null as "null adds an unnecessary typeof return type, that breaks suit from the current pattern (eg NaN, Infinity), and reduces the expressiveness of the language by omitting falsy objects. In other words should typeof NaN be "NaN" instead of number? How do you express 'falsy object'?

From this treatment, Null is a value (in practice), not a type (even thought it really is a primitive type), much like Infinity is a value and not a type. The respective types are object, number.

I'm aware null is not really an object (as you cannot add or edit properties), but one cannot do math with NaN or Infinity, either. They're not really numbers, but it makes sense to put them that 'type'.

var obj = {};
obj instanceof Object
//true
var nil = null;
nil instanceof Object
//false

This always seemed to make sense, because even though null is a primitive, and also an "object value", that means typeof is fine, but it is not an instanceof Object. Suddenly, this makes 4.3.12 "work", categorizing it as object value type, but not an instance of object (because you cannot put properties on it, etc).

In other words, from that perspective with typeof null being "object"; is fine like it is, and is like it is for a good reason, and instanceof Object yielding false is also right.

But this might just be the equivalent of the bootstrap crowd hijacking, and re-implementing the <i> html tag to informally mean "icon" instead of "italic". When the spec is unclear, or dated or just plain bad, developers tend to try to fix it in practice, and for me in practice null is about "null object" (as a value).

A final thought : I remember reading about this some time ago, I found a mailing list about it, with - Axel Rauschmayer, Brendan Eich, Rick Waldron, Mikeal Rogers, and a whole bunch of Smart People™. I decided to read it for fun. I found it again, here . Subject? typeofnull. At one point someone interjects

To me typeof null === 'object' is fine. It makes null a value in the space of 'object'. In practice I see 'null' used to mean "I know this reference (usually an argument) should be an object; I want to pass nothing but signal that I really did mean to pass nothing." The status quo allows this and it seems enough work for null to do for us.

Yes. That. That's what I think, at least that seems logical to me..

TL;DR Kyle is right by most of the world about typeof null is "object" is a bug, but in my opinion it does not seem illogical that it is "object", and in many ways makes more sense (to me) with null as a null value in practice. At the very lease there is an opportunity in this book to address some of this level of confusion about null, which everyone can agree is somewhat unclear.

Testing

Just got through testing all the code samples in Chrome/Firefox/ JSFiddle. Haven't gotten to IE, but I'm sure they're fine.

the-simian commented 10 years ago

Okay, thanks I'll keep that in mind and do 1-3 only.

the-simian commented 10 years ago

Chapter 2: Values


Testing

The code ran as described

the-simian commented 10 years ago

Ch3 : Natives


Testing

All the code ran as described.

getify commented 10 years ago

This is all fantastic feedback and review comments! Thanks so much. Will take a bit to digest and address the various points, but I will make sure to do so. Keep it coming!

getify commented 10 years ago

On the topic of typeof null == "object", I appreciate your in-depth thoughts and reasoning.

Indeed, when I first was doing JS training workshops, I also called null a "special kind of object reference". Not for the reasons you mention, but just so that I could explain the behavior that typeof returned "object", but that you can't visibly get a reference to a null value, at least not obviously. I used to explain these behaviors as: "there's only one special built-in null value, and every time you assign it, you're assigning by reference, etc."

Then Brendan Eich called me out on twitter with the "No, no, it's just a bug." That was enough to snap me out of my disillusion. :)

But more seriously, let me try to address a few of your assertions briefly:

  1. "4.3.11 null value - primitive value that represents the intentional absence of any object value" yeah, that's kinda troubling. TBH, I'd never noticed that "object value" part. I dunno what's up with that. Seems like it's perhaps legacy language.

    But moreover, I think it's important to note that the wording here seems to deviate from the other types, and not necessarily say what null is but more how you might use null. That's subtle, but I think it's enough to water it down to "suggested semantics" rather than "absolute identification".

    In other words, it seems like "a valid suggested way to use null is to represent the absence of an object reference." That's by no means the only way to use null, but it's interesting to note that the spec calls it out as a (at least) a suggestion.

    Ironically, further support for this wording being a "suggestion" comes from your quote of the undefined value:

    primitive value used when a variable has not been assigned a value

    Clearly, this is one way to use undefined, but there's all kinds of places where that's not a complete description. For example, the x = void 1 operator results in assigning the undefined value to x, not "not assiging a value". Similarly, x = function(){}(). In both cases, if x had a value before, it no longer has it, and instead has the undefined value. That's subtly but importantly different to "has not been assigned a value".

    So, again, that wording has to be taken as (at most) "suggested semantics", not exclusive, prescriptive usage.

  2. OTOH, another (I still think perfectly valid) way to use null is to use it as an indistinguishable "alias" of undefined. That's the camp I'm in. I treat both null and undefined as "the absence or emptiness of value". I have seen people try to split them, like "undefined is never had a value yet and null is had a value but doesn't anymore", or even "undefined is has no value and null is has the empty value".

    In my experience, while such distinctions can be made, the benefit of making such distinctions is so weak as to not rise to the level of relevance in my code. In other words, "undefined is the same as null" always seems to me to have a stronger impact on the clarity and readability of my code than the other nuances that I could choose to rely on.

  3. "Often times, an empty reference to an object" Often times? What then do we say about: x = 3; x = null;? Is that an inappropriate use of null, because it's not replacing a previous object reference with an empty object reference, but is rather just replacing a primitive 3 value?

    I suppose you could argue that... that you should only use x = null to "unset" object references, and should use x = undefined to clear out non-object-references. But I've never found that to be a common idiom, and it doesn't seem like that splitting of hairs is going to significantly improve type reasoning in a non-type-enforced language like JS. It seems overly rigorous for not much benefit.

    Maybe I'm missing more nuance here?

  4. Moreover, the reasoning that you imply that null is the typed version of the absence of an object value (as compared to undefined being the typed version of the absence of non-object values) is a little strange to me.

    Let me ask it this way: is the absence of light a form of light we'd call "missing light"? I doubt it. I think we'd say, "either there is light, or there isn't light." Weak analogy, I know, but it's the best I could come up with so far.

    With respect to JavaScript, I don't think it makes sense to talk about the "type of emptiness". It just is "empty". That's why I treat null and undefined as indistinguishable (see above).

  5. "Null is a value (in practice), not a type" This doesn't make sense to me. null is both a value and a type.

    Presently, we're discussing its behavior as a type (the "why or in what circumstances do we use this type" implicit question at hand). That's why our vehicle for the discussion was the typeof null behavior.

    So, the question really is, what do we use the null type for? Once we answer that (subjectively, for ourselves), we then turn to the null value to accomplish it.


Boiling all this down, here's my leaning at the moment:

  1. Since it's indisputable that the spec calls null a type, I still think typeof null == "object" is most accurately described as a bug.
  2. However...

    At the very lease [sic] there is an opportunity in this book to address some of this level of confusion about null, which everyone can agree is somewhat unclear.

    Yep, I think there's enough nuance here that warrants me adding an explanatory note that covers the different perspectives, rather than just glibly glossing over it entirely as I've currently done.

In fact, I think (2) may need to be a section in an appendix, which I will reference from Chapter 1 in a small side note. That way, it doesn't interrupt the flow and reasoning of chapter 1, but it adequately offers deeper explanation for those who care (like you!). :)

getify commented 10 years ago

don't know much about symbol

Yeah, they're brand new. The YDKJS books have to walk a fine line striding between the ES5 world and the (soon coming) ES6 world. Overall, my approach thus far has to sway more toward ES5, but to cover important ES6 things when a contrast is instructive. The fifth title is going to be exclusively "ok, here's all the new world as of ES6", so no straddling there. It'll assume a fully immersive ES6 mindset.

I can read they [sic] as "private properties" to generic objects

Actually, they're not really "private properties" by behavior. The can easily be revealed. That's already some misconception that's spreading (several inaccurate blog posts), and ES6 isn't even out yet!

I'll be covering them more in the fifth title, but there's a brief treatment of them in Chapter 3. They are "special-use properties" more than "private properties". But, admittedly, many devs will probably use them to replace __xyz type prefixed properties, which are usually idiomatically called "private properties".

getify commented 10 years ago

Your number explanation was good, but I expected parseInt() to pop up somewhere, along with a warning about not forgetting your radix.

That's fully covered in Chapter 4. I'll see if there's an appropriate place to insert a note into chapter 2 to reference the coverage in chapter 4.

getify commented 10 years ago

might mention the ~~ "poor mans Math.floor()"

Good catch, this should definitely be mentioned.

getify commented 10 years ago

This whole chapter is a list of things not to do with Native Constructors.

The intent of this chapter is to say, generally: "Don't use these Natives as Constructors", but that's entirely because it tees up Chapter 4 to show why the better use of these Natives is as explicit coercions. Number("42"); // 42 is so much better than new Number("42"); // object. :)

the-simian commented 10 years ago

Thanks for responding so fast, I didn't have time to really proof everything I had composed (I was sort of working in the markdown). Nonetheless, it seems like you've gotten the big picture of What I'm trying to express, and apologies for any typos/sloppiness on my end.

getify commented 10 years ago

ok, chapter 4 is "done" and ready for review. thanks for your patience! :)

getify commented 10 years ago

note: ch4 has had heavy changes across the entire chapter, over the last week, so make sure to a take a fresh eye to the whole thing.

getify commented 10 years ago

ping. ch4 and ch5 are ready for review.

the-simian commented 10 years ago

Thanks for the update! :+1:

getify commented 10 years ago

one last ping! ch5 and now appendix-A are ready for review, if you get a chance. :)

the-simian commented 10 years ago

Chapter 4 Review:

I read this section two times, here are some thoughts I had while reading.


You didn't mention incrementors and decrementors:

+"4" //4 ..coerced 
++"4" //Error
"4"++ //Error

but...
var g ="4";
++g = //5 ..coerced and incremented

var g ="4";
g++ //4 ..coerced, not incremented.

and

var g ="4";
+g++ //4 ..coerced, not incremented.

var g ="4";
++g++ //4 ..Error.

I only bring this up because in addition to concatination, addition and unary + coercion, incrementors and decrementors look very similar and can be unclear. Imagine all your "this is crazy don't write it" examples with ++ and -- also thrown in for good measure.

This matters more if the string to be coerced has no decimal. your example with "3.14" doesn't exploit this behavior as obviously

//spaces in places.
var a = "4";
var b = 9; 
var c = b +++a;
//13
var a = "4";
var b = 9; 
var c = b + ++a;
//14
var a = "4";
var b = 9; 
var c = b + + +a;
//94

//Hyperbolically bad:
var c = "3";
var d = +5+- + +-c++;
//8

[] + [] //""

because these get coerced ToString, and you concatinate "" and "" to get "".

but this is where order matters...

{} + [] //0
[] + {} //"[object Object]"
{} + {} // NaN

we know an object turns into "[object Object]"

var k = {};
k.toString(); "[object Object]"

and we know an array becomes ""

[].toString() //""

so this seems logical.

[] + {} //"[object Object]"

because you'd be concatenating "" and "[object Object]"

But in these cases.. the + isn't just simply concatenating strings, its actually acting like the aforementioned unary + operator and doing implicit coersion.

{} + [] //0
{} + {} // NaN

here is the culprit, its the implicit ToNumber conversion

+[] //0
+{} //NaN

but note it is coercing the second value before the first one. Its not doing it in the order that we read from left to right. so in example 1( {} + []) its actually {} + 0 before it is Nan + []

This section is really large, and you've already dedicated a ton of time to "funky stuff". Maybe pointing out the order of coercion above is just extra noise adding to the choir of "look at this odd behavior". I know the whole point of this section is to highlight that coercion is actually very predictable, and useable and for that I am very thankful. Either way, I figured I'd at least bring this up in review, and if you find it not useful then that makes sense too.

I don't know if this is a good time to mention this, as it may be the wrong place or time or section, but if one wants to actually do "fuzzy" object comparison, they would need to write some custom method to achieve that goal.

Something like this?

function objectsAreEqual(x, y) {
    // I've heavily commented this because equality for js objects is tricky - Jesse
    if (x === y) {
        // if both x and y are null or undefined and exactly the same
        // short circuits primitives.
        return true;
    }

    if (!(x instanceof Object) || !(y instanceof Object)) {
        // if they are not strictly equal, they both need to be Objects
        // instanceof preferable to typeof here.
        return false;
    }

    if (x.constructor !== y.constructor) {
        // they must have the exact same prototype chain, the closest we can do is
        // test for a constructor.
        return false;
    }

    for (var p in x) {
        if (!x.hasOwnProperty(p)) {
            // other properties were tested using x.constructor === y.constructor
            continue;
        }
        if (!y.hasOwnProperty(p)) {
            // allows to compare x[ p ] and y[ p ] when set to undefined
            return false;
        }
        if (x[p] === y[p]) {
            // if they have the same strict value or identity then they are equal
            continue;
        }
        if (typeof(x[p]) !== 'object') {
            // Numbers, Strings, Functions, Booleans must be strictly equal
            return false;
        }
        if (!Object.equals(x[p], y[p])) {
            // Objects and Arrays must be tested recursively
            return false;
        }
    }
    for (p in y) {
        if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) {
            // allows x[ p ] to be set to undefined
            return false;
        }
    }
    return true;
}

Maybe that isn't the way you recommend, and again this might be the wrong place, but since you're discussing == and ===s relationship with objects, I thought Id at least mention it.

the-simian commented 10 years ago

Chapter 4 continued

I should add that for me all the code examples acted as expected. I didn't run them all in ancient versions of IE, but I've encountered the 'radix problem' and other things pre ES5 often enough that I know your examples are as advertised.

The sections build upon each very well. For example, you teach the reader about JSON.stringify and toJSON early on so when those concepts return (such as in Explicitly: * --> Boolean), everything makes a lot of sense.

Best of all this chapter is about what makes Javascript good rather than categorizing it as simple "deeply flawed".

I shudder at thinking JS should start throwing errors all over the place so that try..catch is needed around almost every line. 

As do I. Do you want to drive a motorcycle or a clunky minivan with armor welded on?

The big picture:

This is probably the best written body of work on coercion I've ever read. Its long, but that's OK. Coercion is a big deal in Javascript. The entire tone of teaching the developer how things work rather than only espousing a list of 'naughty things to never do' is really effective.

getify commented 10 years ago

Thanks, as always, for your fantastic and detailed review comments! :)

You didn't mention incrementors and decrementors

See Chapter 5, "Expression Side Effects". I may insert a forward-referencing note to Ch5 here.

+++a; ...... + ++a; ...... + + +a

These are interesting examples I hadn't thought to point out. I may try to mention something brief like that in a note, but definitely in the spirit of "don't do this!".

[] + [] ...... {} + [] ...... [] + {} ...... {} + {}

See Chapter 5, "Blocks". I may insert a forward-referencing note to Ch5 here.

Maybe that isn't the way you recommend, and again this might be the wrong place

Perhaps instead of showing code like this, I should insert a note that many JS libs/frameworks have options for "deepEquality" and explain briefly how they do that. What do you think? Effective enough to clarify the point?

getify commented 10 years ago

FYI: one more substantial section I just added to chapter 4, if you want to glance at it:

https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch4.md#the-curious-case-of-the-

getify commented 10 years ago

If you have any more review comments to add, please re-open and do so! :)