The cause of

Read and learn Effective JavaScript. With my own reading and understanding, I will focus on recording the essence of the content and typesetting the content, so as to facilitate my own review and communication in the future.

Due to the large number of contents, it is divided into each chapter to write the article, and the number of articles in each chapter is different, so each learning notes article shall be subject to the chapter.

Suitable for fragmented reading, compact reading friends. Try to let the little friends finish the series === 85+% of the whole book.

preface

Content overview

  • In the first chapter, beginners can get familiar with JavaScript and understand primitive type, implicit cast, encoding type and other concepts in JavaScript.
  • Chapter 2 focuses on JavaScript variable scope advice, not only explaining how to do it, but also explaining the reasons behind it to help readers understand it better.
  • Chapters 3 and 4 cover functions, objects, and stereotypes, which are at the heart of what sets JavaScript apart from other languages.
  • In chapter 5, the author introduces two common types, array and dictionary, which are easy to be confused, and gives some suggestions to avoid falling into some traps.
  • Chapter 6 covers library and API design;
  • Chapter 7 covers parallel programming, which is a necessary step toward becoming a JavaScript expert

Chapter 4 “Objects and Prototypes”

An object is a basic data structure in JavaScript. Intuitively, an object represents a table of strings mapped to values.

JavaScript, like many object-oriented languages, supports inheritance, which means reusing code or data through a dynamic proxy mechanism. JavaScript’s inheritance mechanism is based on stereotypes, not classes.

In many languages, each object is an instance of a related class that provides shared code across all of its instances. In contrast, JavaScript has no built-in concept of classes. Objects are inherited from other objects, and each object is related to some other object, called its prototype, so prototypes are important to objects.

Understand the difference between prototype, getPrototypeOf, and __proto__

The prototype consists of three separate but related accessors, all with variations on the word prototype.

  • C. Prototype is used to establish bynew C()The prototype of the object created.
  • Object.getprototypeof (obj) is the standard method used in ES5 to obtain the prototype Object of an OBj Object.
  • Obj.__proto__ is a nonstandard method of getting the prototype object of an OBj object.

For these accessors, we imagine a typical JavaScript data type definition. The User constructor needs to be called with the new operator. It takes two parameters, the hash of the name and password, and stores them in the object you create.

function User(name, passwordHash) {
  this.name = name;
  this.passwordHash = passwordHash;
}
User.prototype.toString = function() {
  return "[User]" + this.name + "]";
};
User.prototype.checkPassword = function(password) {
  return hash(password)
};

const u = new User("Lin"."0ef33ae791068ec64b502d6cb0191387");
Copy the code

prototype

The User function takes a default prototype property, and we added two methods toString and checkPassword to that property object. When an instance of User is created using the new operator, the resulting object U gets the automatically allocated prototype object, which is stored in User.prototype.

Prototype object u implements the inheritance relationship with the prototype object user. prototype through prototype. When the instance object does not find the corresponding property on its own, it then looks for the prototype object of U, such as the property stored in user.prototype.

Object.getPrototypeOf()

The prototype property of the constructor is used to set the prototype relationship for the new instance. The function object.getPrototypeof () in ES5 can be used to retrieve the prototype of an existing Object, as in the example above:

Object.getPrototypeOf(u) === User.prototype; // true

__proto__

Some environments provide a non-standard way to retrieve an object’s prototype, namely a special __proto__ attribute. This can be used as a stopgap in environments where ES5’s object.getPrototypeof () method is not supported.

u.__proto__ === USer.prototype; // true

conclusion

JavaScript programmers tend to describe User as a class, even though it is little more than a function. Your class in JavaScript is essentially a constructor User combined with a prototype object that shares methods between instances of the class User.prototype, an internal implementation of methods shared between instances.

  • C.prototypeAttributes arenew C()The prototype of the object created.
  • Object.getPrototypeOf(obj)Is the standard function in ES5 to retrieve object stereotypes.
  • obj.__proto__Is a nonstandard method of retrieving object prototypes.
  • A class is a design pattern consisting of a constructor and an associated stereotype.

Use object. getPrototypeof functions instead of __proto__ properties

ES5 introduces the Object.getProtoTypeof function as a standard API for retrieving Object prototypes, as opposed to using the special __proto__ attribute to retrieve Object prototypes. Not all JavaScript environments support retrieving the prototype of an object via the __proto__ attribute, which is not compatible.

For example, objects with null archetypes are handled differently in different environments. In some environments, the __proto__ attribute is inherited from Object.prototype, so objects with null archetypes do not have this special __proto__ attribute.

In any case, the Object.getProtoTypeof function is valid and is a more standard and portable way to extract Object prototypes. And the __proto__ attribute pollutes all objects, thus causing a lot of bugs.

Object JavaScript environments that do not provide this ES5 API can easily use the object.getProtoTypeof function using the __proto__ attribute.

if (typeof Object.getPrototypeOf === "undefined") {
  Object.getPrototypeOf = function(obj) {
    const t = typeof obj;
    if(! obj || (t ! = ="object"&& t ! = ="function")) {
      throw new TypeError("not an object");
    }
    returnobj.__proto__; }}Copy the code

This implementation avoids setting the Object.getProtoTypeof function even if it already exists.

conclusion

  • Use a standardObject.getPrototypeOfFunctions instead of using non-standard ones__protoProperties.
  • In support of__proto__Property in a non-ES5 environmentObject.getPrototypeOfFunction.

Rule 32: Never modify the __proto__ attribute

Possible problems caused by modifying the __proto__ attribute

The __proto__ attribute has the ability to modify an object’s prototype link. Because not all platforms support the feature of changing object prototypes, portability can be a problem.

Another problem is that performance issues, depth of all modern JavaScript engines are optimized to get and set the attributes of the object, the optimization is based on the understanding of object structure, when change the internal structure of an object (e.g., add or delete the object or the attributes of the object in the prototype chain), will disable some optimization. Modifying the _propto_ property actually changes the inheritance structure itself, so modifying the _proto_ property results in more optimization failures than normal property modifications.

The biggest reason to avoid modifying the _proto_ attribute is to keep the behavior predictable. The stereotype chain of an object defines its behavior through a set of defined attributes and their values. Modifying the stereotype chain of an object destroys the entire inheritance hierarchy of the object. Keeping the inheritance hierarchy relatively stable is a basic guideline.

Object.create

The objct. create function in ES5 creates a new Object with a custom prototype chain. For environments that do not support ES5, article 33 provides a portable implementation of object. creat that does not rely on the _proto_ attribute.

conclusion

  • Never modify the object_proto_ properties.
    • Portability issues.
    • Performance issues.
    • Unpredictability of behavior resulting from breaking inheritance hierarchies.
  • useObject.createThe function sets a custom prototype for a new object.

Rule 33: Make constructors independent of new operators

When a User creates a constructor for User, the program relies on the new operator to call the constructor. Note that the function assumes that the recipient is a brand new object. If the caller forgets to use the new keyword, the receiver of the function will be the global object.

function User(name, password) {
  this.name = name;
  this.passwordHash = password;
}

const u = User('lin'.'123456');
u;	// undefined
this.name;	// 'lin'
this.password;	/ / '123456'
Copy the code

When used with the new operator, it works as expected. However, it fails when called as a normal function, which can be avoided by checking at the beginning of the function that the recipient of the function is a correct User instance. This way, whether the User function is called as a function or as a constructor, it returns an object inherited from User.prototype.

function User(name, password) {
  if(! (this instanceof User)) {
    return new User(name, password);
  }
  this.name = name;
  this.passwordHash = password;
}

const u1 = User('lin'.'123456');
const u2 = new User('lin'.'123456');

u1 instanceof User;	// true
u2 instanceof User;	// true
Copy the code

The downside of this pattern is that it requires an extra function call to be called back, which is a bit expensive. So you can use ES5’s object. create function to return a new Object inherited from User.prototype with the name and password attributes initialized.

function User(name, password) {
  const self = this instanceof User 
  	? this 
  	: Object.create(User.prototype);
  self.name = name;
  self.passwordHash = password;
  return self;
}

Copy the code

Object. Create works only in ES5 environments, and in some older environments it is possible to replace Object. Create by creating a local constructor and initializing that constructor with the new operator.

if (typeof Object.create === 'undefined') {
  Object.create = function(prototype) {
    function C() {}
    C.prototype = prototype;
    return newC(); }}Copy the code

conclusion

  • Make the constructor independent of the calling syntax by calling itself in the constructor definition using the new operator or the object.create method.
  • When a function is expected to be called using the new operator, it needs to be compatible and documented.
  • You can use Object.create and be compatible with it.

Rule 34: Store methods in prototypes

Instance methods

If you implement the User class in the above 30 items without defining any special methods in its stereotype.

// Store the method in the prototype in article 30
function User(name, passwordHash) {
  this.name = name;
  this.passwordHash = passwordHash;
}
User.prototype.toString = function() {
  return "[User]" + this.name + "]";
};
User.prototype.checkPassword = function(password) {
  return hash(password)
};

const u = new User("Lin"."0ef33ae791068ec64b502d6cb0191387");

// Store methods in the instance
function User(name, passwordHash) {
  this.name = name;
  this.passwordHash = passwordHash;
  this.toString = function() {
    return "[User]" + this.name + "]";
  };
  this.checkPassword = function(password) {
    return hash(password)
  };
}
Copy the code

Most of the time, the behavior of the two classes is consistent, but when we construct multiple instances of User, the second constructor creates copies of the toString and checkPassword methods for each instance.

// There will be 6 function objects
const u1 = new User();
const u2 = new User();
const u2 = new User();
Copy the code

The first method, the toString and checkPassword methods, are created only once, and are shared between object instances through prototypes. And modern JavaScript engines are deeply optimized for prototype lookup, so copying methods to instance objects doesn’t necessarily guarantee a significant speed increase, but instance methods certainly take up more memory than prototype methods.

conclusion

  • Storing a method in an instance object creates multiple copies of the function, so there is one copy for each instance object.
  • Storing common methods in prototypes is better than storing them in instance objects.

35. Use closures to store private data

JavaScript has no enforcement mechanism for treating private properties, and any program can simply access the property name to get the corresponding object property. As for… In, the ES5 Object. The keys () and Object. The getOwnPropertyNames character such as () function can be easily access to all of the properties of an Object name.

Some programmers treat private attributes with coding conventions, such as using naming conventions to prefix or follow private attribute names with an underscore character (_), but the object is still free to change its implementation.

For programs that require a higher degree of information hiding, JavaScript provides a reliable mechanism for information hiding — closures. You can store data in closed variables without providing direct access to those variables, and the only way to get the internal structure of a closure is if the function explicitly exposes the way to get it.

Objects and closures have opposite strategies: objects’ properties are automatically exposed, while variables in closures are automatically hidden.

So we can store private data in variables in constructors and convert the object’s methods into closures that reference those variables.

function User(name, passwordHash) {
  this.toString = function() {
    return "[User " + name + "]";
  };
  this.checkPassword = function(password) {
    return hash(password) === passwordHash;
  };
}
Copy the code

As a result, name and passwordHash are hidden by method closures, and instances of User do not contain any instance attributes at all, so external code cannot directly access the name and passwordHash variables of the User instance.

One disadvantage of this approach is that, because of variable scope, these methods cannot be shared with prototypes and must be placed in instance objects, which can lead to proliferation of method copies. But for scenarios where securing information is more important, it’s worth it.

conclusion

  • Closure variables are private and can only be obtained by local references.
  • Hiding information through methods that treat local variables as private data.

Rule 36: Only instance state is stored in instance objects

There is a one-to-many relationship between a prototype object and an instance. Note that data for each instance cannot be stored in its prototype, resulting in data contamination between instances.

So sharing state data among prototypes can cause problems by sharing methods across multiple instances of a class, method passes are stateless, and this is used to refer to instance state.

conclusion

  • Sharing mutable data can be problematic because stereotypes are shared by all of their instances.
  • Store mutable instance state in instance objects.
  • Stateless shared methods are typically mounted on prototypes.

Rule 37: Recognize the implicit binding problem with this variable

Look at a snippet of code that reads a CSV file:

// omit CSVReader constructor...
CSVReader.prototype.read = function(str) {
  const lines = str.trim().split(/\n/);
  return lines.map(function() {
    return line.split(this.regexp);	/ / error!})}const reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n");	// [["a,b,c"], ["d,e,f"]];
Copy the code

The destroy function passed to line.map refers to the regexp property of this. Regexp expected to fetch the CSVReader instance, but map binds the receiver of its destroy function to the Lines array. The Lines array does not have a regexp property, causing the undefined value to result in an error.

Because every function has an implicit binding to the this variable. The binding value of this variable is determined when the function is called; if the function is not explicitly called, this is implicitly bound to the nearest enclosing function.

CSVReader.prototype.read = function(str) {
  const lines = str.trim().split(/\n/);
  return lines.map(function() {
    return line.split(this.regexp);
  }, this)	// map passes this as the second argument
}
const reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n");	
// [["a","b","c"], ["d","e","f"]];
Copy the code

So you can use the map function’s second argument to pass the external this bound to the callback function. If the function has no extra arguments, then we can store a reference to the extra external this binding in a lexical scoped variable.

CSVReader.prototype.read = function(str) {
  const lines = str.trim().split(/\n/);
  const self = this;
  return lines.map(function() {
    return line.split(self.regexp);	// Find the external this through the scope chain})}const reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n");	
// [["a","b","c"], ["d","e","f"]];
Copy the code

You can also use the bind method of a higher-order function for your own use

CSVReader.prototype.read = function(str) {
  const lines = str.trim().split(/\n/);
  return lines.map(function() {
    return line.split(self.regexp);
  }.bind(this));	// Explicitly bind with bind
}
const reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n");	
// [["a","b","c"], ["d","e","f"]];
Copy the code

conclusion

  • The scope of this variable is always determined by its nearest enclosing function.
  • Use a local variable (lexical scope) self, me, or that to make the this binding available for internal functions.
  • Use higher-order function methods to explicitly bind this object.

Rule 39: Do not reuse parent class attribute names

Give each class a unique id attribute as follows.

function Actor() {
  this.id = ++Actor.nextId;
}
function Alien() {
  Actor.call(this);
  this.id = ++Alien.nextId;
}
Actor.nextId = 0;
Alien.nextId = 0;
Copy the code

In this code, both classes attempt to write data to the instance attribute ID, which each class considers “private,” but which is stored on the instance object and named a string pointing to the same attribute. Therefore, subclasses must pay attention to all attributes used by their parent class and avoid generic attribute names.

function Actor() {
  this.id = ++Actor.nextId;
}
function Alien() {
  Actor.call(this);
  this.alienId = ++Alien.nextId;	/ / id
}
Actor.nextId = 0;
Alien.nextId = 0;
Copy the code

conclusion

  • Note all attribute names used by the parent class to avoid reuse conflicts.

Rule 40: Avoid inheriting standard classes

ECMAScript has many important standard classes, such as Array, Function, and Date. They are defined with so many special behaviors that it is difficult to write subclasses that behave correctly. For example, creating an abstract directory class and inheriting all the behavior of an array:

function Dir(path, entries) {
  this.path = path;
  for (const i = 0; i < entries.length; i < n; i++) {
    this[i] = entries[i];	// Assign an array to the instance}}// Inherits the Array standard class
Dir.prototype = Object.create(Array.prototype);
const dir = new Dir('/'[1.2.3.4]);
// Array class attribute error
dir.length;	/ / 0
Copy the code

Because the Array standard Class Length property only works on special objects that are marked internally as “real” arrays, the ECMAScript standard states that it is an invisible internal property called [[Class]].

JavaScript does not have a secret inner Class system. The value of [[Class]] is simply a label. Array objects are created using the Array constructor or the [] syntax with a [[Class]] attribute of the value “Array”. If a Function is appended with a [[Class]] attribute of “Function”. So JavaScript keeps the Length attribute in sync with the number of special objects whose internal attribute [[Class]] is “Array”. When we extend the Array Class, instances of subclasses are not created using new Array() or literal [] syntax. The Dir instance’s [[Class]] property value is “Object”.

[[CLass]] Construction
Array new Array(), []
Boolean new Boolean()
Date new Date()
Error new Error(), … A series of error types
Function new Function(), function(){}
JSON JSON
Math Math
Number new Number()
Object new object(), {}, new MyClass
RegExp new RegExp(), /… /
String new String()

The default Object. The prototype. The toString method can query the receiver’s internal [[Class]] attribute to create a general description of objects.

const dir = new Dir('/'[]);Object.prototype.toString.call(dir);	// "[object Object]"
Object.prototype.toString.call([]);	// "[object Array]"
Copy the code

In the ECMAScript standard library, some properties or methods of a standard Class expect the correct [[Class]] property or other special internal properties that subclasses do not provide, so avoid inheriting the following standard classes: Array, Boolead, Date, Function, Number, RegExp, String.

conclusion

  • Inherited standard classes are often broken due to special internal attributes.
  • Instead of inheriting classes, use attribute delegates.

Rule 41: Think of prototypes as implementation details

An object provides its users with a lightweight, simple, and powerful set of operations that do not care where properties are stored in the stereotype inheritance structure. An object might then be implemented with a property at a different point in the object prototype chain, but as long as its value remains constant, so does the behavior of these basic operations. In short, a prototype is an implementation detail of an object’s behavior.

* * JavaScript send convenient reflection Object. The prototype. The hasOwnProperty method to determine whether an attribute is the Object instance attributes, completely ignore prototype inheritance structure. ** Features such as Object.getPrototypeOf and __proto__ allow programmers to traverse the prototype chain of an Object and query its prototype Object individually.

JavaScript does not distinguish between public and private properties of objects, so rely on documentation and constraints. If a library provides object properties that are not documented or explicitly marked as internal, it is best for consumers to leave those properties alone.

conclusion

  • An object is an interface, and a prototype is an implementation.
  • Avoid the prototype structure of objects that you have no control over.
  • Avoid implementing properties inside objects that you have no control over.

Rule 42: Avoid using rash monkey patches

The monkey patch

Since objects share stereotypes, each object can add, remove, or modify the attributes of the stereotype, which is often referred to as a monkey patch, and its appeal lies in its power. Problems arise when multiple libraries patch the same prototype in incompatible ways.

// Monkey patch 1
Array.prototype.split = function(i) {
  return [this.slic(0, i), this.slice(i)];
}
// Monkey patch 2
Array.prototype.split = function(i) {
  const i = Math.floor(this.length / 2);
  return [this.slic(0, i), this.slice(i)];
}
Copy the code

The two libraries that patch the prototype in conflicting ways cannot be used in the same program. An alternative is: ** If the library merely fixes the prototype as a convenience, these changes can be placed in a function that the user can call or ignore. ** This actually doesn’t depend on array.prototype. split.

function addArrayMethod() {
  Array.prototype.split = function(i) {
    return [this.slic(0, i), this.slice(i)]; }}Copy the code

polyfill

While monkey patches are dangerous, there is another particularly reliable and valuable use scenario: Polyfill. Standard API for compatibility with JavaScript programs and libraries on different platforms or browser versions.

For example, ES5 defines array methods (map, filter, and so on), which may not be supported by some browser versions. Since these behaviors are standardized, multiple libraries can provide implementations of the same standard methods, avoiding the risk of library-to-library incompatibility. Here is a simple version compatibility:

if (typeof Array.porotype.map ! = ="function") {
  Array.prototype.map = function(f, thisArg) {
    const result = [];
    for (const i = 0, n = this.length; i < n; i++) {
      result[i] = f.call(thisArg, this[i], i);
		}
    returnresult; }}Copy the code

conclusion

  • Avoid using rash monkey patches.
  • Log all monkey patches executed by the library to avoid conflicting errors.
  • Consider an alternative way to make the monkey patch succeed by putting it in an export function.
  • Use monkey patches to provide polyfills for missing standard apis.