Original text: dmitripavlutin.com/7-tips-to-h…

Translator: Front-end wisdom

Click “like” and then look, wechat search [Big Move the world] pay attention to this person without dACHang background, but with a positive attitude upward. In this paper, making github.com/qq449245884… Has been included, the article has been categorized, also organized a lot of my documentation, and tutorial materials.

Everyone said there was no project on your resume, so I found one and gave it away【 Construction tutorial 】.

About 8 years ago, when the original author started learning ABOUT JS, he encountered a strange situation where there was both a value of undefined and a value of null. What is the obvious difference between them? They all seem to define null values, and the comparison of null == undefined evaluates to true.

Most modern languages like Ruby, Python, or Java have a nil (or null) value, which seems like a reasonable approach.

With JavaScript, the interpreter returns undefined when accessing an uninitialized variable or object property. Such as:

let company;
company;    // => undefined
let person = { name: 'John Smith' };
person.age; // => undefined
Copy the code

Null, on the other hand, represents a missing object reference, and JS itself does not set variables or object properties to NULL.

Some native methods, such as string.prototype.match (), can return NULL to represent missing objects. Take a look at the following example:

let array = null;  
array;                // => null
let movie = { name: 'Starship Troopers',  musicBy: null };
movie.musicBy;        // => null
'abc'.match(/[0-9]/); // => null 
Copy the code

Because of the permissive nature of JS, it’s easy for developers to access uninitialized values, and I made the same mistake.

Often, this dangerous operation generates undefined errors that end the script quickly. Common error messages are:

  • TypeError: 'undefined' is not a function
  • TypeError: Cannot read property '<prop-name>' of undefined
  • type errors

JS developers can understand the irony of this joke:

function undefined() {
  // problem solved
}
Copy the code

To reduce the risk of such errors, it is important to understand how undefined is generated. It is more important to suppress its occurrence and prevent propagation in your application, thereby increasing the persistence of your code.

Let’s discuss undefined and its impact on code security in more detail.

What the hell is undefined

There are six basic types of JS

  • Boolean: truefalse
  • Number: 1, 6.7, 0 XFF
  • String: "Gorilla and banana"
  • Symbol: Symbol("name") (starting ES2015)
  • Null: null
  • Undefined: undefined.

And a separate Object type :{name: “Dmitri”}, [“apple”, “orange”].

From the six primitive types, undefined is a special value that has its own undefined type, according to the ECMAScript specification.

The default value is undefined when a variable is not assigned.

The standard explicitly defines that a value of undefined is received when accessing uninitialized variables, nonexistent object attributes, nonexistent array elements, and so on. For example,

let number;
number;     // => undefined

let movie = { name: 'Interstellar' };
movie.year; // => undefined

let movies = ['Interstellar', 'Alexander'];
movies[3];  // => undefined
Copy the code

General process of the above code:

  • An uninitialized variablenumber
  • An object property that does not existmovie.year
  • Or there is no movies3 element

Is defined as undefined.

The ECMAScript specification defines the type of undefined

Undefined type is a type whose only value is Undefined.

In this sense, typeof undefined returns “undefined” string

typeof undefined === 'undefined'; // => true    
Copy the code

Of course typeof does a good job of verifying that variables contain the value of undefined

let nothing;
typeof nothing === 'undefined';   // => true  
Copy the code

2. Create common scenarios that are not defined

2.1 Uninitialized variables

Declared variables that have not been assigned (initialized) default to undefined.

let myVariable;
myVariable; // => undefined    
Copy the code

MyVariable is declared but not assigned. The default value is undefined.

An effective way to solve the problem of uninitialized variables is to assign initial values whenever possible. The fewer variables in the uninitialized state, the better. Ideally, you can specify a value immediately after declaring const myVariable =’Initial value’, but this is not always possible.

Tip 1: Use let and const instead of var

In my opinion, one of the best features of ES6 is the new way to declare variables using const and let. Const and let have block scope (as opposed to the old function scope var) and both exist in a temporary dead zone until a row is declared.

When a variable receives a value once and permanently, it is recommended to use a const declaration, which creates an immutable binding.

A nice feature of const is that we must assign an initial value to the variable const myVariable =’initial’. Variables are not exposed to an uninitialized state, and accessing undefined is not possible.

The following example checks a function that verifies whether a word is a palindrome:

function isPalindrome(word) { const length = word.length; const half = Math.floor(length / 2); for (let index = 0; index < half; index++) { if (word[index] ! == word[length - index - 1]) { return false; } } return true; } isPalindrome('madam'); // => true isPalindrome('hello'); // => falseCopy the code

The length and half variables are assigned once. It seems reasonable to declare them as const, because these variables do not change.

If you need to rebind variables (that is, multiple assignments), apply the let declaration. Whenever possible, give it an initial value immediately, for example, let index = 0.

What about using var declarations, as opposed to ES6, the advice is to stop using them altogether.

Variables declared by var are promoted to the top of the function scope. You can declare the var variable somewhere at the end of the function scope, but you can still access it before you declare it: the value of the corresponding variable is undefined.

In contrast, a variable declared with let or const cannot be accessed before it is declared. This happens because the variable is in a temporary dead zone before it is declared. This is good because there is less chance of accessing undefined.

The above example, updated with let (rather than VAR), raises ReferenceError because variables in the temporary dead zone cannot be accessed.

function bigFunction() {
  // code...
  myVariable; // => Throws 'ReferenceError: myVariable is not defined'
  // code...
  let myVariable = 'Initial value';
  // code...
  myVariable; // => 'Initial value'
}
bigFunction();
Copy the code

Tip 2: Increase cohesion

Cohesion describes the degree to which a module’s elements (namespaces, classes, methods, code blocks) are clustered together. A measure of cohesion is often referred to as high cohesion or low cohesion.

High cohesion is preferred because it suggests designing the elements of a module to focus only on a single task, which constitutes a module.

  • Focused and understandable: It is easier to understand the functionality of modules

  • Maintainable and easier to refactor: Changes in a module affect fewer modules

  • Reusable: Focusing on a single task makes modules easier to reuse

  • Testable: Modules that focus on a single task can be more easily tested

High cohesion and low coupling are characteristics of a well-designed system.

The code block itself may be thought of as a small module, and in order to achieve as high cohesion as possible, the variables need to be as close as possible to the code block where they are used.

For example, if a variable exists only to form a block scope, do not expose the variable to the outer block scope, because the outer block should not care about the variable.

A classic example of unnecessarily extending the life of a variable is the use of a for loop in a function:

function someFunc(array) {
  var index, item, length = array.length;
  // some code...
  // some code...
  for (index = 0; index < length; index++) {
    item = array[index];
    // some code...
  }
  return 'some result';
}
Copy the code

The index, item, and length variables are declared at the beginning of the function body, but they are used only at the end, so what’s wrong with this approach?

The variables index and item are uninitialized from the top declaration to the for statement, with the value undefined. They have unreasonably long lifetimes throughout the function scope.

A better approach is to move these variables as far as possible to where they are used:

function someFunc(array) {
  // some code...
  // some code...
  const length = array.length;
  for (let index = 0; index < length; index++) {
    const item = array[index];
    // some 
  }
  return 'some result';
}
Copy the code

The index and item variables exist only in the scope of the for statement, and have no meaning outside of it. The length variable is also declared close to where it is used.

Why is the modified version better than the original version? The main points are:

  • Variables do not expose the undefined state, so there is no risk of accessing undefined

  • Moving variables as close to where they should be used increases the readability of the code

  • Highly cohesive blocks of code are easier to refactor and extract into separate functions if necessary

2.2 Accessing non-existent properties

JS returns undefined when accessing an object property that does not exist.

Let’s use an example to illustrate this point:

let favoriteMovie = {
  title: 'Blade Runner'
};
favoriteMovie.actors; // => undefined
Copy the code

FavoriteMovie is an object with a single property title. Favoritemovie. actors accessing nonexistent actors is counted as undefined.

Accessing nonexistent properties itself does not raise an error, but attempts to retrieve data from nonexistent property values do. TypeError: Cannot read property of undefined.

Modify the previous code snippet slightly to illustrate the TypeError throw:

let favoriteMovie = {
  title: 'Blade Runner'
};
favoriteMovie.actors[0];
// TypeError: Cannot read property '0' of undefined
Copy the code

FavoriteMovie has no attribute, so favoriteMovie. Actors are undefined. Therefore, accessing the first item of undefined using the expression favoritemovie. actors[0] raises TypeError.

JS allows access to properties that do not exist. This allows access to be confusing: properties may or may not be set. The ideal way to circumvent this problem is to restrict an object to always defining the properties it holds.

Unfortunately, we often have no control over objects. These objects may have different sets of attributes in different scenarios, so all of these scenarios must be handled manually:

Next, we implement a function append(array, toAppend), whose main function is to add new elements to the beginning and/or end of an array. The toAppend argument accepts objects with attributes:

  • First: The beginning of the array into which the element is inserted

  • Last: Element is inserted at the end of the array.

The function returns a new instance of the array, leaving the original array unchanged (that is, it is a pure function).

The first version of append() looks simple, like this:

function append(array, toAppend) {
  const arrayCopy = array.slice();
  if (toAppend.first) {
    arrayCopy.unshift(toAppend.first);
  }
  if (toAppend.last) {
    arrayCopy.push(toAppend.last);
  }
  return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]
append(['Hello'], { last: 'World' });     // => ['Hello', 'World']
append([8, 16], { first: 4 });            // => [4, 8, 16]
Copy the code

Because a toAppend object can omit first or last attributes, you must verify that these attributes are present in toAppend. If the property does not exist, the property accessor value is undefined.

Check whether the first or last attribute is undefined and verify with conditions like if(toAppendix. First){} and if(toAppendix. Last){} :

One disadvantage of this method is that undefined, false, null, 0, NaN and ” are virtual values.

In the current implementation of Append (), this function does not allow virtual elements to be inserted:

append([10], { first: 0, last: false }); / / = > [10]Copy the code

0 and false are imaginary. Because if(toappend.first){} and if(toappend.last){} are actually compared to Falsy, these elements are not inserted into the array, and the function returns the initial array [10] without making any modifications.

The following tips explain how to properly check for the presence of attributes.

Tip 3: Check if the attribute exists

JS provides a number of methods to determine whether an object has a particular property:

  • Obj prop! == undefined: directly compares with undefined

  • Typeof obj prop! ==’undefined’ : validates the attribute value type

  • Obj.hasownproperty (‘prop’) : Verifies that an object has its own properties

  • ‘Prop’ in obj: Verifies that an object has its own properties or inherited properties

My recommendation is to use the in operator, which has a short and snappy syntax. The presence of the IN operator indicates an explicit intent to check whether an object has a particular attribute, without accessing the actual attribute value.

Another good solution is obj.hasownProperty (‘prop’), which is slightly longer than the IN operator and only validates in the object’s own properties.

The remaining two approaches involving a comparison with undefined may be valid, but in my opinion, obJ. Prop! == undefined and typeof obj.prop! ==’undefined’ looks verbose and weird, and exposes the dubious path of handling undefined directly.

Let’s use the in operator to improve the append(array, toAppend) function:

function append(array, toAppend) {
  const arrayCopy = array.slice();
  if ('first' in toAppend) {
    arrayCopy.unshift(toAppend.first);
  }
  if ('last' in toAppend) {
    arrayCopy.push(toAppend.last);
  }
  return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]
append([10], { first: 0, last: false });  // => [0, 10, false]
Copy the code

‘First’ in toAppend (and ‘last’ in toAppend) is true if the corresponding attribute exists, false otherwise. The use of the in operator solves the problem of inserting virtual elements 0 and false. Now, adding these elements at the beginning and end of [10] will produce the expected result [0,10,false].

Tip 4: Deconstruct access object properties

When accessing an object property, you sometimes need to indicate the default value if the property does not exist. You can do this using in and the ternary operators.

const object = { };
const prop = 'prop' in object ? object.prop : 'default';
prop; // => 'default'
Copy the code

As the number of properties to check increases, the use of the ternary operator syntax becomes daunting. For each attribute, a new line of code must be created to handle the default values, adding to an ugly wall of look-alike ternary operators.

For a more elegant approach, you can use deconstruction of ES6 objects.

Object deconstruction allows you to extract object attribute values directly into variables and set default values if the attribute does not exist, avoiding the convenient syntax of dealing with undefined directly.

In fact, attribute extraction now seems short and meaningful:

const object = {  };
const { prop = 'default' } = object;
prop; // => 'default'
Copy the code

To see what’s in action, let’s define a useful function that wraps a string in quotes. Quote (subject, config) takes the first parameter as the string to wrap. The second argument, config, is an object with the following properties:

  • Char: Wrapped characters, such as’ (single quote) or ‘(double quote), default to’.

  • Skipifletter letter: Skips the quoted Boolean value if the string is already referenced, defaults to true.

Using the advantages of object destructor, let’s implement quote()

function quote(str, config) {
  const { char = '"', skipIfQuoted = true } = config;
  const length = str.length;
  if (skipIfQuoted
      && str[0] === char
      && str[length - 1] === char) {
    return str;
  }
  return char + str + char;
}
quote('Hello World', { char: '*' });        // => '*Hello World*'
quote('"Welcome"', { skipIfQuoted: true }); // => '"Welcome"'
Copy the code

Const {char = ‘”, skipifQuote = true} = config Deconstructing assignment extracts the char and SkipifQuote attributes from the config object in one line. If some properties in the config object are not available, the destruct assignment will set the default values :char to ‘”‘ and skipifQuote to false.

The feature still has room for improvement. Let’s move the deconstruction assignment directly to the parameters section. And set a default value for the config parameter (the empty object {}) to skip the second parameter if the default setting is sufficient.

function quote(str, { char = '"', skipIfQuoted = true } = {}) {
  const length = str.length;
  if (skipIfQuoted
      && str[0] === char
      && str[length - 1] === char) {
    return str;
  }
  return char + str + char;
}
quote('Hello World', { char: '*' }); // => '*Hello World*'
quote('Sunny day');                  // => '"Sunny day"'
Copy the code

Note that the destruct assignment replaces the function config argument. I like this: Quote () is shortened by one line. On the right side of the deconstruction assignment, make sure to use an empty object without specifying a second argument at all.

Object deconstruction is a powerful feature that effectively handles extracting properties from objects. I like the possibility of specifying a default value to return if the accessed property does not exist. This avoids undefined and the problems associated with handling it.

Tip 5: Populate objects with default properties

If you don’t need to create a variable for each attribute as you would for a deconstructed assignment, objects missing certain attributes can be filled with default values.

ES6 Object.assign (target, source1, source2…) This function copies the values of all enumerable own properties from one or more source objects to the target object, which is returned.

For example, you need to access the properties of an unsafeOptions object, which does not always contain its complete set of properties.

To avoid accessing non-existent properties from unsafeOptions, let’s make some adjustments:

  • Define defaults objects that contain default property values

  • Call Object.assign ({}, defaults, unsafeOptions) to build the new Object options. The new object receives all properties from unsafeOptions, but the missing properties are retrieved from the Defaults object.

    const unsafeOptions = {
      fontSize: 18
    };
    const defaults = {
      fontSize: 16,
      color: 'black'
    };
    const options = Object.assign({}, defaults, unsafeOptions);
    options.fontSize; // => 18
    options.color;    // => 'black'
Copy the code

UnsafeOptions contains only the fontSize property. The defaults object defines the default values for the attributes fontSize and color.

Object.assign() takes the first argument as the target Object {}. The target object receives the value of the fontSize property from the unsafeOptions source object. And the human defaults object gets the color property value, because unsafeOptions does not contain the color property.

The order in which you enumerate source objects is important: subsequent source object properties override previous source object properties.

It is now safe to access any properties of the Options object, including options.color, which was not available in the original unsafeOptions.

Another easy way to do this is to use the ES6 expansion operator:

const unsafeOptions = { fontSize: 18 }; const defaults = { fontSize: 16, color: 'black' }; const options = { ... defaults, ... unsafeOptions }; options.fontSize; // => 18 options.color; // => 'black'Copy the code

Object initializers extend properties from source objects in defaults and unsafeOptions. It is important to specify the order of the source objects, since the later source object properties override the previous source objects.

Populating incomplete objects with default property values is an effective strategy for making code safe and durable. In either case, the object always contains the complete set of attributes: and you cannot generate attributes that are undefined.

2.3 Function Parameters

Function arguments implicitly default to undefined.

In general, functions defined with a specific number of arguments should be called with the same number of arguments. In this case, the parameter gets the desired value

function multiply(a, b) { a; // => 5 b; // => 3 return a * b; } multiply(5, 3); / / = > 15Copy the code

Multiply (5,3) is called so that parameters A and B receive the corresponding 5 and 3 values, returning the result :5 * 3 = 15.

What happens when you omit arguments when calling?

function multiply(a, b) {
  a; // => 5
  b; // => undefined
  return a * b;
}
multiply(5); // => NaN
Copy the code

The multiply(a, b){} function is defined by two parameters a and b. The multiply(5) call is executed with one argument: the result is that one argument is 5, but the b argument is undefined.

Tip 6: Use default parameter values

Sometimes a function does not need the full set of arguments to call and can simply set default values for arguments that have no values.

Recalling the previous example, let’s make an improvement by assigning a default value of 2 to the b argument if it is undefined:

function multiply(a, b) { if (b === undefined) { b = 2; } a; // => 5 b; // => 2 return a * b; } multiply(5); / / = > 10Copy the code

Although the provided method for assigning default values is valid, direct comparisons with undefined values are not recommended. It’s verbose and looks like a hack.

The ES6 defaults can be used here:

function multiply(a, b = 2) { a; // => 5 b; // => 2 return a * b; } multiply(5); // => 10 multiply(5, undefined); / / = > 10Copy the code

2.4 Function Return Value

Implicitly, without a return statement, the JS function returns undefined.

In JS, functions without any return statements implicitly return undefined:

function square(x) {
  const res = x * x;
}
square(2); // => undefined  
Copy the code

The square() function does not return the result of the calculation; the result of the function call is undefined.

When a return statement is not followed by an expression, undefined is returned by default.

function square(x) {
  const res = x * x;
  return;
}
square(2); // => undefined
Copy the code

return; The statement is executed, but it does not return any expression, and the result of the call is undefined.

function square(x) { const res = x * x; return res; } square(2); / / = > 4Copy the code

Tip 7: Don’t trust auto-insert semicolons

The following statement list in JS must start with a semicolon (;) The end:

  • An empty statement

  • Let, const, var, the import and export declarations

  • Express statement

  • The debugger statement

  • Continue statement, break statement

  • Throw statement

  • Return statement

If one of the above statements is used, please do your best to indicate a semicolon at the end:

function getNum() { let num = 1; return num; } getNum(); / / = > 1Copy the code

Semicolons are mandatory at the end of let and return statements.

What happens when you don’t want to write these semicolons? For example, let’s say we want to reduce the size of the source file.

In this case, ECMAScript provides an automatic semicolon insertion (ASI) mechanism to insert missing semicolons for you.

With ASI’s help, you can remove the semicolon from the previous example: function getNum() { // Notice that semicolons are missing let num = 1 return num } getNum() // => 1

The above code is valid JS code, and the missing semicolon ASI is inserted automatically for us.

At first glance, it looks nice. The ASI mechanism allows you to write fewer unnecessary semicolons, making JS code smaller and easier to read.

ASI creates a small but annoying trap. When a line break is between return and return \n expression, ASI automatically inserts a semicolon before the line break (return; \ n expression).

Function internal return; ? That is, the function returns undefined. The unexpected return of undefined can cause unexpected problems if you don’t understand the mechanics of ASI in detail.

Call getPrimeNumbers() to return the value:

function getPrimeNumbers() {
  return 
    [ 2, 3, 5, 7, 11, 13, 17 ]
}
getPrimeNumbers() // => undefined
Copy the code

There is a line break between the return statement and the array. JS automatically inserts a semicolon after the return statement.

function getPrimeNumbers() { return; [2, 3, 5, 7, 11, 13, 17]; } getPrimeNumbers(); // => undefinedCopy the code

return; Causes the function getPrimeNumbers() to return undefined instead of the expected array.

This problem is solved by removing the line break between return and array literals:

function getPrimeNumbers() {
  return [ 
    2, 3, 5, 7, 11, 13, 17 
  ];
}
getPrimeNumbers(); // => [2, 3, 5, 7, 11, 13, 17]
Copy the code

My suggestion is to investigate the exact way automatic semicolon insertion can be done to avoid this situation.

Of course, never place a newline character between a return and a returned expression.

2.5 Void operator

Void

Returns undefined regardless of the result of the evaluation.

void 1;                    // => undefined
void (false);              // => undefined
void {name: 'John Smith'}; // => undefined
void Math.min(1, 3);       // => undefined
Copy the code

One use case for the void operator is to limit expression evaluation to undefined, depending on some side effects of evaluation.

3. Undefined array

When you access an array element with an out-of-bounds index, you get undefined.

const colors = ['blue', 'white', 'red'];
colors[5];  // => undefined
colors[-1]; // => undefined
Copy the code

The colors array has three elements, so the valid indexes are 0,1, and 2.

Because indexes 5 and -1 have no array elements, the values for accessing colors[5] and colors[-1] are undefined.

JS, you may encounter what are called sparse arrays. These arrays are gapped arrays, that is, in some indexes, no elements are defined.

When you access a gap (also known as an empty slot) in a sparse array, you also get a undefined.

The following example generates sparse arrays and attempts to access their empty slots

const sparse1 = new Array(3);
sparse1;       // => [<empty slot>, <empty slot>, <empty slot>]
sparse1[0];    // => undefined
sparse1[1];    // => undefined
const sparse2 = ['white',  ,'blue']
sparse2;       // => ['white', <empty slot>, 'blue']
sparse2[1];    // => undefined
Copy the code

When using arrays, to avoid fetching undefined, make sure to use valid array indexes and avoid creating sparse arrays.

4. The difference between undefined and null

A reasonable question arises: What is the main difference between undefined and NULL? Both special values are represented as null.

The main difference is that undefined denotes the value of an uninitialized variable, and null denotes the deliberate absence of an object.

Let’s explore the differences through some examples.

Number is defined but not assigned.

let number;
number; // => undefined
Copy the code

The number variable is not defined, which clearly indicates an uninitialized variable.

The same uninitialized concept occurs when accessing non-existent object properties

const obj = { firstName: 'Dmitri' };
obj.lastName; // => undefined
Copy the code

Because there is no lastName attribute in obj, JS correctly evaluates obj.lastName as undefined.

In other cases, you know that variables are expected to hold an object or a function to return an object. But for some reason, you can’t instantiate the object. In this case, NULL is a meaningful indicator of the missing object.

For example, clone() is a function that clones an ordinary JS object and returns an object

function clone(obj) { if (typeof obj === 'object' && obj ! == null) { return Object.assign({}, obj); } return null; } clone({name: 'John'}); // => {name: 'John'} clone(15); // => null clone(null); // => nullCopy the code

However, clone() can be called with a non-object argument: 15 or null(or usually a raw value, null or undefined). In this case, the function cannot create a clone, so it returns NULL, an indicator of the missing object.

The Typeof operator distinguishes between these two values

typeof undefined; // => 'undefined'
typeof null;      // => 'object'
Copy the code

The strict equality operator === correctly distinguishes undefined from null:

let nothing = undefined;
let missingObject = null;
nothing === missingObject; // => false
Copy the code

conclusion

Undefined exists as a result of the permitted nature of JS, which allows the use of:

  • An uninitialized variable
  • An object property or method that does not exist
  • Access the array element of an out-of-bounds index
  • The result of a call to a function that does not return any result

In most cases, a direct comparison with undefined is a bad idea. An effective strategy is to reduce the use of undefined in code:

  • Reduce the use of uninitialized variables

  • Shorten the lifetime of a variable and get closer to where it is used

  • Assign initial values to variables whenever possible

  • Play around with const and let

  • Use default values for nonessential function arguments

  • Verify that the property exists or populate an unsafe object with the default property

  • Avoid using sparse arrays

The bugs that may exist after code deployment cannot be known in real time. In order to solve these bugs, I spent a lot of time on log debugging. Incidentally, HERE I recommend a good BUG monitoring tool Fundebug.

communication

This article is updated every week, you can search wechat “big move the world” for the first time to read and urge more (one or two earlier than the blog hey), this article GitHub github.com/qq449245884… It has been included and sorted out a lot of my documents. Welcome Star and perfect. You can refer to the examination points for review in the interview.