introduce
This chapter is the second chapter about ECMAScript object-oriented implementation. In the first chapter, we are discussing the comparison between Introduction and CEMAScript. If you have not read the first chapter, before proceeding with this chapter, I strongly recommend that you read the first chapter first, because this chapter is too long (page 35).
Original English text: http://dmitrysoshnikov.com/ecmascript/chapter-7-2-oop-ecmascript-implementation/
Note: Because the article is too long, errors are inevitable and they are constantly being corrected.
In the introduction, we extend to ECMAScript. Now, when we know that it is OOP implementation, let’s define it accurately:
The code copy is as follows:
ECMAScript is an object-oriented programming language supporting delegating inheritance based on prototypes.
ECMAScript is an object-oriented language that supports prototype-based delegated inheritance.
We will analyze the most basic data types. The first thing we need to understand is that ECMAScript uses primitive values and objects to distinguish entities. Therefore, the "in JavaScript, everything is an object" mentioned in some articles is wrong (not completely correct), and the primitive value is some data types we are going to discuss here.
Data Type
Although ECMAScript is a dynamic weak-type language that can dynamically convert types, it still has data types. In other words, an object must belong to a real type.
There are 9 data types defined in the standard specification, but only 6 are directly accessible in ECMAScript programs. They are: Undefined, Null, Boolean, String, Number, and Object.
The other three types can only be accessed at the implementation level (these types cannot be used by ECMAScript objects) and are used for specifications to explain some operational behavior and save intermediate values. These 3 types are: Reference, List and Completion.
Therefore, Reference is used to explain operators such as delete, typeof, and this, and contains a base object and an attribute name; List describes the behavior of the parameter list (when new expressions and function calls); Completion is used to explain behavior break, continue, return and throw statements.
Primitive value type
Looking back at the data types used in ECMAScript programs in 6, the first 5 are primitive value types, including Undefined, Null, Boolean, String, Number, and Object.
Example of original value type:
The code copy is as follows:
var a = undefined;
var b = null;
var c = true;
var d = 'test';
var e = 10;
These values are implemented directly at the bottom layer, they are not objects, so there is no prototype, no constructor.
Uncle Note: Although these native values are similar to those we usually use (Boolean, String, Number, Object) but are not the same thing. Therefore, the results of typeof(true) and typeof(Boolean) are different, because the result of typeof(Boolean) is a function, so the functions Boolean, String, and Number have prototypes (the following read and write attributes chapter will also be mentioned).
If you want to know which type of data is the best, it is best to use typeof. There is an example that needs to be noted. If you use typeof to judge the type of null, the result is object. Why? Because the type of null is defined as Null.
The code copy is as follows:
alert(typeof null); // "object"
The reason for displaying "object" is because the specification stipulates this: return "object" for the typeof string value of the Null value.
The specification does not imagine explaining this, but Brendan Eich (inventor of JavaScript) noticed that null is mostly used for objects to appear relative to undefined, such as setting an object to a null reference. However, some documents have some annoying to attribute it to a bug, and put the bug in the bug list that Brendan Eich also participated in the discussion. The result is that if you let it go, you should set the result of typeof null to object (although the 262-3 standard defines the type of null as Null, and the 262-5 has changed the standard to null as an object).
Object type
Next, the Object type (not to be confused with the Object constructor, now only the abstract type is discussed) is the only data type that describes the ECMAScript object.
Object is an unordered collection of key-value pairs.
Object is an unordered collection containing key-value pairs
The key value of an object is called a property, and the property is a container of the original value and other objects. If the value of a property is a function, we call it a method.
For example:
The code copy is as follows:
var x = { // The object "x" has 3 properties: a, b, c
a: 10, // Original value
b: {z: 100}, // The object "b" has an attribute z
c: function () { // Function (method)
alert('method x.c');
}
};
alert(xa); // 10
alert(xb); // [object Object]
alert(xbz); // 100
xc(); // 'method x.c'
Dynamic
As we pointed out in Chapter 17, objects in ES are completely dynamic. This means that when the program is executed, we can add, modify or delete the properties of the object at will.
For example:
The code copy is as follows:
var foo = {x: 10};
// Add new attributes
foo.y = 20;
console.log(foo); // {x: 10, y: 20}
// Modify the property value to a function
foo.x = function () {
console.log('foo.x');
};
foo.x(); // 'foo.x'
// Delete attributes
delete foo.x;
console.log(foo); // {y: 20}
Some properties cannot be modified - (read-only properties, deleted properties, or unconfigurable properties). We will explain it in the attribute characteristics later.
In addition, the ES5 specification stipulates that static objects cannot extend new attributes, and their property pages cannot be deleted or modified. They are so-called frozen objects, which can be obtained by applying the Object.freeze(o) method.
The code copy is as follows:
var foo = {x: 10};
// Freeze the object
Object.freeze(foo);
console.log(Object.isFrozen(foo)); // true
// Cannot be modified
foo.x = 100;
// Cannot expand
foo.y = 200;
// Cannot be deleted
delete foo.x;
console.log(foo); // {x: 10}
In the ES5 specification, the Object.preventExtensions(o) method is also used to prevent extensions, or the Object.defineProperty(o) method is used to define properties:
The code copy is as follows:
var foo = {x : 10};
Object.defineProperty(foo, "y", {
value: 20,
writable: false, // Read only
configurable: false // Not configurable
});
// Cannot be modified
foo.y = 200;
// Cannot be deleted
delete foo.y; // false
// Prevention and control expansion
Object.preventExtensions(foo);
console.log(Object.isExtensible(foo)); // false
// Cannot add new attributes
foo.z = 30;
console.log(foo); {x: 10, y: 20}
Built-in objects, native objects and host objects
It is necessary to note that the specification also distinguishes these built-in objects, element objects and host objects.
Built-in objects and element objects are defined and implemented by the ECMAScript specification, and the difference between the two is trivial. All ECMAScript implementing objects are native objects (some of which are built-in objects, some are created when the program is executed, such as user-defined objects). A built-in object is a subset of the native object and is built into ECMAScript before the program starts (for example, parseInt, Match, etc.). All host objects are provided by the host environment, usually a browser, and may include, for example, window, alert, etc.
Note that the host object may be implemented by ES itself and fully conforms to the normative semantics. In this regard, they can be called "native host" objects (as soon as possible), but the norm does not define the concept of "native host" objects.
Boolean, String and Number objects
In addition, the specification also defines some native special wrapper classes, these objects are:
1. Boolean object
2. String Object
3. Digital Objects
These objects are created by the corresponding built-in constructor and contain native values as their internal properties. These objects can be converted to save the original value and vice versa.
The code copy is as follows:
var c = new Boolean(true);
var d = new String('test');
var e = new Number(10);
// Convert to original value
// Use functions without new keyword
с = Boolean(c);
d = String(d);
e = Number(e);
// Reconvert to object
с = Object(c);
d = Object(d);
e = Object(e);
In addition, there are objects created by special built-in constructors: Function (function object constructor), Array (array constructor), RegExp (regular expression constructor), Math (mathematics module), Date (date constructor), etc. These objects are also values of Object object types. Their differences are managed by internal properties. We will discuss these things below.
Literal
For the values of three objects: object, array and regular expression, they have abbreviated identifiers called object initializer, array initializer, and regular expression initializer:
The code copy is as follows:
// Equivalent to new Array(1, 2, 3);
// Or array = new Array();
// array[0] = 1;
// array[1] = 2;
// array[2] = 3;
var array = [1, 2, 3];
// Equivalent to
// var object = new Object();
// object.a = 1;
// object.b = 2;
// object.c = 3;
var object = {a: 1, b: 2, c: 3};
// Equivalent to new RegExp("^//d+$", "g")
var re = /^/d+$/g;
Note that if the above three objects are reassigned to a new type, the subsequent implementation semantics are used according to the new type. For example, in the current Rhino and the old version of SpiderMonkey 1.7, objects will be created successfully with the constructor of the new keyword, but the semantics of some implementations (current Spider/TraceMonkey) literals may not change after the type is changed.
The code copy is as follows:
var getClass = Object.prototype.toString;
Object = Number;
var foo = new Object;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
var bar = {};
// Rhino, SpiderMonkey 1.7- 0, "[object Number]"
// Others: still "[object Object]", "[object Object]"
alert([bar, getClass.call(bar)]);
// Array has the same effect
Array = Number;
foo = new Array;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
bar = [];
// Rhino, SpiderMonkey 1.7- 0, "[object Number]"
// Others: still "", "[object Object]"
alert([bar, getClass.call(bar)]);
// But for RegExp, the semantics of literals are not changed. semantics of the literal
// isn't being changed in all tested implementations
RegExp = Number;
foo = new RegExp;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
bar = /(?!)/g;
alert([bar, getClass.call(bar)]); // /(?!)/g, "[object RegExp]"
Regex literals and RegExp objects
Note that in the following two examples, in the specification of the third edition, the semantics of regular expressions are equivalent. The regexp literals only exist in one sentence and are created in the parsing stage. However, the RegExp constructor creates a new object, so this may cause some problems, such as the lastIndex value is wrong during testing:
The code copy is as follows:
for (var k = 0; k < 4; k++) {
var re = /ecma/g;
alert(re.lastIndex); // 0, 4, 0, 4
alert(re.test("ecmascript")); // true, false, true, false
}
// Compare
for (var k = 0; k < 4; k++) {
var re = new RegExp("ecma", "g");
alert(re.lastIndex); // 0, 0, 0, 0
alert(re.test("ecmascript")); // true, true, true, true
}
Note: However, these issues have been corrected in the ES specification in the 5th edition. Regardless of whether they are based on literals or constructors, they create new objects.
Associative array
Various static discussions of text, JavaScript objects (often created with object initializer {}) are called hash tables and hash tables or other simple titles: hash (concepts in Ruby or Perl), management arrays (concepts in PHP), dictionaries (concepts in Python), etc.
There are only such terms, mainly because their structures are similar, that is, using "key-value" pairs to store objects, which fully conforms to the data structure defined by the "association array" or "hash table" theory. In addition, hash table abstract data types are usually used at the implementation level.
However, although the concept is described in terms of terms, this is actually a mistake. From the perspective of ECMAScript: ECMAScript has only one object, type and its subtype, which is no different from "key-value" to storage, so there is no special concept on this. Because the internal properties of any object can be stored as key-value" pairs:
The code copy is as follows:
var a = {x: 10};
a['y'] = 20;
az = 30;
var b = new Number(1);
bx = 10;
by = 20;
b['z'] = 30;
var c = new Function('');
cx = 10;
cy = 20;
c['z'] = 30;
// etc., subtype of any object "subtype"
Furthermore, since objects can be empty in ECMAScript, the concept of "hash" is also incorrect here:
The code copy is as follows:
Object.prototype.x = 10;
var a = {}; // Create empty "hash"
alert(a["x"]); // 10, but not empty
alert(a.toString); // function
a["y"] = 20; // Add new key-value pair to "hash"
alert(a["y"]); // 20
Object.prototype.y = 20; // Add prototype attributes
delete a["y"]; // Delete
alert(a["y"]); // But here the key and value are still worth 20
Please note that the ES5 standard allows us to create unprototyped objects (implemented using the Object.create(null) method). From this perspective, such objects can be called hash tables:
The code copy is as follows:
var aHashTable = Object.create(null);
console.log(aHashTable.toString); // Undefined
Additionally, some properties have specific getter/setter methods, so it can also lead to confusion about this concept:
The code copy is as follows:
var a = new String("foo");
a['length'] = 10;
alert(a['length']); // 3
However, even if the "hash" might have a "prototype" (for example, a class that delegates hash objects in Ruby or Python), this term is not correct in ECMAScript because there is no semantic difference between the two notations (i.e., the dot notation ab and a["b"] notation).
The concept of "property attribute" in ECMAScript is not semantically separated from "key", array index, and methods. Here, all the attribute reading and writing of objects must follow a unified rule: check the prototype chain.
In the following Ruby example, we can see the semantic difference:
The code copy is as follows:
a = {}
a.class # Hash
a.length # 0
# new "key-value" pair
a['length'] = 10;
# Semantically, the attribute or method that is accessed with a point, not a key
a.length # 1
# The indexer accesses the key in the hash
a['length'] # 10
# It's similar to dynamically declaring a Hash class on an existing object
# Then declare the new attribute or method
class Hash
def z
100
end
end
# New properties are accessible
az # 100
# But not "key"
a['z'] # nil
The ECMA-262-3 standard does not define the concept of "hash" (and similar). However, if there is such a structural theory, the object named after this may be.
Object conversion
To convert an object into a primitive value, you can use the valueOf method. As we said, when the constructor of the function is called as a function (for some types), but if you do not use the new keyword, you convert the object into the original value, it is equivalent to an implicit valueOf method call:
The code copy is as follows:
var a = new Number(1);
var primitiveA = Number(a); // Implicit "valueOf" call
var alsoPrimitiveA = a.valueOf(); // explicit call
alert([
typeof a, // "object"
type of primitiveA, // "number"
typeof alsoPrimitiveA // "number"
]);
This method allows objects to participate in various operations, such as:
The code copy is as follows:
var a = new Number(1);
var b = new Number(2);
alert(a + b); // 3
// even
var c = {
x: 10,
y: 20,
valueOf: function () {
return this.x + this.y;
}
};
var d = {
x: 30,
y: 40,
// The same function as c's valueOf
valueOf: c.valueOf
};
alert(c + d); // 100
The default value of valueOf will change according to the type of the object (if not overridden). For some objects, it returns this - for example: Object.prototype.valueOf(), and also the calculated value: Date.prototype.valueOf() returns date and time:
The code copy is as follows:
var a = {};
alert(a.valueOf() === a); // true, "valueOf" returns this
var d = new Date();
alert(d.valueOf()); // time
alert(d.valueOf() === d.getTime()); // true
In addition, the object has a more primitive representation - string display. This toString method is reliable and it is used automatically in some operations:
The code copy is as follows:
var a = {
valueOf: function () {
return 100;
},
toString: function () {
return '__test';
}
};
// In this operation, the toString method is automatically called
alert(a); // "__test"
// But here, the valueOf() method is called
alert(a + 10); // 110
// However, once valueOf is deleted
// toString can be called automatically again
delete a.valueOf;
alert(a + 10); // "_test10"
The toString method defined on Object.prototype has special significance, and it returns the internal [[Class]] attribute value that we will discuss below.
Compared with converting to original values (ToPrimitive), there is also a conversion specification (ToObject) to convert values into object types.
An explicit method is to use the built-in Object constructor as a function to call ToObject (something like using the new keyword is OK):
The code copy is as follows:
var n = Object(1); // [object Number]
var s = Object('test'); // [object String]
// Some similarities are also possible with the new operator
var b = new Object(true); // [object Boolean]
// If you apply the parameter new Object, you create a simple object
var o = new Object(); // [object Object]
// If the parameter is an existing object
// The result of creation is to simply return the object
var a = [];
alert(a === new Object(a)); // true
alert(a === Object(a)); // true
Regarding calling built-in constructors, there are no common rules for using or not to apply the new operator, depending on the constructor. For example, Array or Function uses a constructor of the new operator or a simple function that does not use the new operator to produce the same result:
The code copy is as follows:
var a = Array(1, 2, 3); // [object Array]
var b = new Array(1, 2, 3); // [object Array]
var c = [1, 2, 3]; // [object Array]
var d = Function(''); // [object Function]
var e = new Function(''); // [object Function]
When some operators are used, there are also some display and implicit conversions:
The code copy is as follows:
var a = 1;
var b = 2;
// Implicit
var c = a + b; // 3, number
var d = a + b + '5' // "35", string
// Explicit
var e = '10'; // "10", string
var f = +e; // 10, number
var g = parseInt(e, 10); // 10, number
// etc
Properties of attributes
All properties can have many attributes.
1.{ReadOnly} - Ignore the write operation of assigning values to attributes, but read-only attributes can be changed by the host environment behavior - that is, they are not "constant values";
2.{DontEnum}--Attribute cannot be enumerated by for..in loop
3.{DontDelete}--The behavior of the delete operator is ignored (that is, it cannot be deleted);
4.{Internal} - Internal attribute, without a name (only used at the implementation level), such attributes cannot be accessed in ECMAScript.
Note that in ES5 {ReadOnly}, {DontEnum} and {DontDelete} are renamed to [[Writable]], [[Enumerable]] and [[Configurable]], and these properties can be managed manually through Object.defineProperty or similar methods.
The code copy is as follows:
var foo = {};
Object.defineProperty(foo, "x", {
value: 10,
writable: true, // i.e. {ReadOnly} = false
enumerable: false, // i.e. {DontEnum} = true
configurable: true // i.e. {DontDelete} = false
});
console.log(foo.x); // 10
// Get the attributes through the description
var desc = Object.getOwnPropertyDescriptor(foo, "x");
console.log(desc.enumerable); // false
console.log(desc.writable); // true
// etc
Internal properties and methods
Objects can also have internal properties (part of the implementation level), and ECMAScript programs cannot access directly (but we will see below that some implementations allow access to some such properties). These properties are accessed through nested brackets [[ ]]. Let's look at some of these properties. The description of these properties can be found in the specification.
Each object should implement the following internal properties and methods:
1.[[Prototype]] - the prototype of the object (which will be introduced in detail below)
2.[[Class]]—a representation of a string object (for example, Object Array, Function Object, Function, etc.); used to distinguish objects
3.[[Get]]--The method to obtain attribute values
4.[[Put]]--The method of setting attribute values
5.[[CanPut]] - Check whether the attribute is writable
6.[[HasProperty]]-check whether the object already has this property
7.[[Delete]] - Delete this property from the object
8.[[DefaultValue]] returns the original value of the object for (call the valueOf method, and some objects may throw a TypeError exception).
The value of the internal property [[Class]] can be obtained indirectly through the Object.prototype.toString() method, which should return the following string: "[object " + [[Class]] + "]" . For example:
The code copy is as follows:
var getClass = Object.prototype.toString;
getClass.call({}); // [object Object]
getClass.call([]); // [object Array]
getClass.call(new Number(1)); // [object Number]
// etc
This function is usually used to check objects, but in specification, the [[Class]] of the host object can be any value, including the value of the [[Class]] attribute of the built-in object, so theoretically it cannot be 100% accurate. For example, the [[Class]] property of the document.childNodes.item(...) method returns "String" in IE, but the returned "Function" in other implementations is indeed.
The code copy is as follows:
// in IE - "String", in other - "Function"
alert(getClass.call(document.childNodes.item));
Constructor
So, as we mentioned above, objects in ECMAScript are created through so-called constructors.
The Constructor is a function that creates and initializes the newly created object.
A constructor is a function that creates and initializes newly created objects.
Object creation (memory allocation) is the responsibility of the internal method of the constructor [[Construct]]. The behavior of this internal method is defined, and all constructors use this method to allocate memory for new objects.
Initialization is managed by calling the function up and down the new object, which is responsible by the internal method of the constructor [[Call]].
Note that the user code can only be accessed in the initialization phase, although we can return different objects during the initialization phase (ignoring the tihs objects created in the first phase):
The code copy is as follows:
function A() {
// Update newly created objects
this.x = 10;
// But the return is a different object
return [1, 2, 3];
}
var a = new A();
console.log(ax, a); undefined, [1, 2, 3]
Referring to Chapter 15 Functions - Algorithm Section for Creating Functions, we can see that the function is a native object, including the [[Construct]] ] and [[Call]] ] properties as well as the prototype of the prototype displayed - the prototype of the future object (Note: NativeObject is a convention for native object native objects, used in the pseudo-code below).
The code copy is as follows:
F = new NativeObject();
F.[[Class]] = "Function"
.... // Other attributes
F.[[Call]] = <reference to function> // function itself
F.[[Construct]] = internalConstructor // Ordinary internal constructor
.... // Other attributes
// Object prototype created by F constructor
__objectPrototype = {};
__objectPrototype.constructor = F // {DontEnum}
F.prototype = __objectPrototype
[[Call]] ] is the main way to distinguish objects except the [[Class]] attribute (which is equivalent to "Function" here), so the internal [[Call]] attribute of the object is called as a function. If such an object uses the typeof operator, it returns "function". However, it is mainly related to native objects. In some cases, the implementation uses typeof to get the value differently, such as the effect of window.alert (...) in IE:
The code copy is as follows:
// IE browser - "Object", "object", other browsers - "Function", "function"
alert(Object.prototype.toString.call(window.alert));
alert(typeof window.alert); // "Object"
The internal method [[Construct]] is activated by using a constructor with a new operator, as we said, is responsible for memory allocation and object creation. If there are no parameters, the brackets for calling the constructor can also be omitted:
The code copy is as follows:
function A(x) { // constructor А
this.x = x || 10;
}
// If you do not pass parameters, brackets can also be omitted
var a = new A; // or new A();
alert(ax); // 10
// explicitly pass parameter x
var b = new A(20);
alert(bx); // 20
We also know that the shis in the constructor (initialization phase) is set to the newly created object.
Let's study the algorithm for object creation.
Object creation algorithm
The behavior of the internal method [[Construct]] can be described as follows:
The code copy is as follows:
F.[[Construct]](initialParameters):
O = new NativeObject();
// The property [[Class]] is set to "Object"
O.[[Class]] = "Object"
// Get the object g when referring to F.prototype
var __objectPrototype = F.prototype;
// If __objectPrototype is an object, then:
O.[[Prototype]] = __objectPrototype
// Otherwise:
O.[[Prototype]] = Object.prototype;
// Here O.[[Prototype]] is the prototype of the Object object
// F.[[Call]] is applied when the newly created object is initialized
// Set this as newly created object O
// The parameters are the same as the initialParameters in F
R = F.[[Call]](initialParameters); this === O;
// Here R is the return value of [[Call]]
// In JS, like this:
// R = F.apply(O, initialParameters);
// If R is an object
return R
// Otherwise
return O
Please note two main features:
1. First, the prototype of the newly created object is obtained from the prototype property of the function at the moment (this means that the prototype properties of the two created objects created by the same constructor can be different because the prototype properties of the function can also be different).
2. Secondly, as we mentioned above, if [[Call]] returns the object when the object is initialized, this is exactly the result used for the entire new operator:
The code copy is as follows:
function A() {}
A.prototype.x = 10;
var a = new A();
alert(ax); // 10 Get it from the prototype
// Set the .prototype property to a new object
// Why explicitly declared .constructor attribute will be explained below
A.prototype = {
constructor: A,
y: 100
};
var b = new A();
// The object "b" has new properties
alert(bx); // undefined
alert(by); // 100 get from the prototype
// But the prototype of object a can still get the original result
alert(ax); // 10 - Get from the prototype
function B() {
this.x = 10;
return new Array();
}
// If the "B" constructor does not return (or returns this)
// Then this object can be used, but the following situation returns an array
var b = new B();
alert(bx); // undefined
alert(Object.prototype.toString.call(b)); // [object Array]
Let's learn more about the prototype
prototype
Each object has a prototype (except some system objects). Prototype communication is carried out through internal, implicit, and indirect access to [[Prototype]] prototype properties. The prototype can be an object or a null value.
Property constructor
The above example has two important knowledge points. The first is about the prototype property of the constructor property of the function. In the algorithm of function creation, we know that the constructor property is set as the prototype property of the function during the function creation stage. The value of the constructor property is an important reference to the function itself:
The code copy is as follows:
function A() {}
var a = new A();
alert(a.constructor); // function A() {}, by delegation
alert(a.constructor === A); // true
Usually in this case, there is a misunderstanding: it is wrong to construct a property as the newly created object itself, but, as we can see, this property belongs to the prototype and accesses the object through inheritance.
By inheriting an instance of the constructor attribute, you can indirectly obtain a reference to the prototype object:
The code copy is as follows:
function A() {}
A.prototype.x = new Number(10);
var a = new A();
alert(a.constructor.prototype); // [object Object]
alert(ax); // 10, by prototype
// The same effect as a.[[Prototype]].x
alert(a.constructor.prototype.x); // 10
alert(a.constructor.prototype.x === ax); // true
But please note that the constructor and prototype properties of the function can be redefined after the object is created. In this case, the object loses the mechanism mentioned above. If you edit the prototype prototype of an element through the function's prototype attribute (add new objects or modify existing objects), you will see the newly added attribute on the instance.
However, if we completely change the prototype property of the function (by assigning a new object), the reference to the original constructor is lost, because the object we create does not include the constructor property:
The code copy is as follows:
function A() {}
A.prototype = {
x: 10
};
var a = new A();
alert(ax); // 10
alert(a.constructor === A); // false!
Therefore, prototype references to functions need to be restored manually:
The code copy is as follows:
function A() {}
A.prototype = {
constructor: A,
x: 10
};
var a = new A();
alert(ax); // 10
alert(a.constructor === A); // true
Note that although the constructor attribute is manually restored, compared with the original missing prototype, the {DontEnum} feature is gone, that is, the for..in loop statement in A.prototype is no longer supported, but in the 5th edition specification, the [[Enumerable]] feature provides the ability to control the enumerable state enumerable through the [[Enumerable]] feature.
The code copy is as follows:
var foo = {x: 10};
Object.defineProperty(foo, "y", {
value: 20,
enumerable: false // aka {DontEnum} = true
});
console.log(foo.x, foo.y); // 10, 20
for (var k in foo) {
console.log(k); // only "x"
}
var xDesc = Object.getOwnPropertyDescriptor(foo, "x");
var yDesc = Object.getOwnPropertyDescriptor(foo, "y");
console.log(
xDesc.enumerable, // true
yDesc.enumerable // false
);
Explicit prototype and implicit [[Prototype]] properties
Generally, it is incorrect to explicitly refer to the prototype of an object through the function's prototype property. It refers to the same object, the [[Prototype]] property of the object:
a.[[Prototype]] ---> Prototype <---- A.prototype
In addition, the [[Prototype]] value of the instance is indeed obtained on the prototype property of the constructor.
However, submitting the prototype attribute will not affect the prototype that has been created (it will only affect when the prototype attribute of the constructor changes), that is, the newly created object has a new prototype, and the created object will still refer to the original old prototype (this prototype can no longer be modified).
The code copy is as follows:
// The situation before modifying the A.prototype prototype
a.[[Prototype]] ---> Prototype <---- A.prototype
// After modification
A.prototype ---> New prototype // New object will have this prototype
a.[[Prototype]] ---> Prototype // On the original prototype of the boot
For example:
The code copy is as follows:
function A() {}
A.prototype.x = 10;
var a = new A();
alert(ax); // 10
A.prototype = {
constructor: A,
x: 20
y: 30
};
// Object a is a value obtained from the crude oil's prototype through an implicit [[Prototype]] reference
alert(ax); // 10
alert(ay) // undefined
var b = new A();
// But the new object is the value obtained from the new prototype
alert(bx); // 20
alert(by) // 30
Therefore, it is wrong for some articles to say that "dynamic modification of prototypes will affect all objects that have new prototypes", and the new prototype only takes effect on newly created objects after the prototype is modified.
The main rule here is: the prototype of the object is created when the object is created, and cannot be modified into a new object after that. If the same object is still referenced, it can be referenced through the explicit prototype of the constructor. After the object is created, the properties of the prototype can only be added or modified.
Non-standard __proto__ attributes
However, some implementations (such as SpiderMonkey) provide unstandard __proto__ explicit properties to reference the object's prototype:
The code copy is as follows:
function A() {}
A.prototype.x = 10;
var a = new A();
alert(ax); // 10
var __newPrototype = {
constructor: A,
x: 20,
y: 30
};
// Reference to a new object
A.prototype = __newPrototype;
var b = new A();
alert(bx); // 20
alert(by); // 30
// The "a" object is still using the old prototype
alert(ax); // 10
alert(ay); // undefined
// Explicitly modify the prototype
a.__proto__ = __newPrototype;
// Now the "а" object refers to a new object
alert(ax); // 20
alert(ay); // 30
Note that ES5 provides the Object.getPrototypeOf(O) method, which directly returns the [[Prototype]] property of the object - the initial prototype of the instance. However, compared to __proto__, it is just a getter, which does not allow set value.
The code copy is as follows:
var foo = {};
Object.getPrototypeOf(foo) == Object.prototype; // true
Object independent of constructor
Because the prototype of the instance is independent of the constructor and the prototype properties of the constructor, the constructor can delete it after completing its main work (creating an object). The prototype object continues to exist by referring to the [[Prototype]] attribute:
The code copy is as follows:
function A() {}
A.prototype.x = 10;
var a = new A();
alert(ax); // 10
// Set A to null - Show reference constructor
A = null;
// But if the .constructor property has not changed,
// You can still create objects through it
var b = new a.constructor();
alert(bx); // 10
// Implicit references are also deleted
delete a.constructor.prototype.constructor;
delete b.constructor.prototype.constructor;
// The constructor of A can no longer create objects
// But these two objects still have their own prototypes
alert(ax); // 10
alert(bx); // 10
Characteristics of instanceof operator
We display the reference prototype through the constructor's prototype property, which is related to the instanceof operator. This operator works with the prototype chain, not the constructor, and with this in mind, there are often misunderstandings when detecting objects:
The code copy is as follows:
if (foo instanceof Foo) {
...
}
This is not used to detect whether the object foo is created with the Foo constructor. All instanceof operators only require one object property - foo.[[Prototype]]. In the prototype chain, check whether it exists from Foo.prototype. The instanceof operator is activated through the internal method [[HasInstance]] in the constructor.
Let's take a look at this example:
The code copy is as follows:
function A() {}
A.prototype.x = 10;
var a = new A();
alert(ax); // 10
alert(a instanceof A); // true
// If the prototype is set to null
A.prototype = null;
// …"a" can still access the prototype through a.[[Prototype]]
alert(ax); // 10
// However, the instanceof operator cannot be used normally anymore
// Because it is implemented from the constructor's prototype property
alert(a instanceof A); // Error, A.prototype is not an object
On the other hand, the object can be created by the constructor, but if the value of the [[Prototype]] property of the object is set the same as the prototype property of the constructor, the instanceof will return true when checking:
The code copy is as follows:
function B() {}
var b = new B();
alert(b instanceof B); // true
function C() {}
var __proto = {
constructor: C
};
C.prototype = __proto;
b.__proto__ = __proto;
alert(b instanceof C); // true
alert(b instanceof B); // false
Prototypes can store methods and share attributes
Most programs use prototypes to store the methods, default states, and shared objects' properties.
事实上,对象可以拥有自己的状态,但方法通常是一样的。 因此,为了内存优化,方法通常是在原型里定义的。 这意味着,这个构造函数创建的所有实例都可以共享找个方法。
The code copy is as follows:
function A(x) {
this.x = x || 100;
}
A.prototype = (function () {
// 初始化上下文
// 使用额外的对象
var _someSharedVar = 500;
function _someHelper() {
alert('internal helper: ' + _someSharedVar);
}
function method1() {
alert('method1: ' + this.x);
}
function method2() {
alert('method2: ' + this.x);
_someHelper();
}
// 原型自身
return {
constructor: A,
method1: method1,
method2: method2
};
})();
var a = new A(10);
var b = new A(20);
a.method1(); // method1: 10
a.method2(); // method2: 10, internal helper: 500
b.method1(); // method1: 20
b.method2(); // method2: 20, internal helper: 500
// 2个对象使用的是原型里相同的方法
alert(a.method1 === b.method1); // true
alert(a.method2 === b.method2); // true
读写属性
正如我们提到,读取和写入属性值是通过内部的[[Get]]和[[Put]]方法。这些内部方法是通过属性访问器激活的:点标记法或者索引标记法:
The code copy is as follows:
// 写入
foo.bar = 10; // 调用了[[Put]]
console.log(foo.bar); // 10, 调用了[[Get]]
console.log(foo['bar']); // 效果一样
让我们用伪代码来看一下这些方法是如何工作的:
[[Get]]方法
[[Get]]也会从原型链中查询属性,所以通过对象也可以访问原型中的属性。
O.[[Get]](P):
The code copy is as follows:
// 如果是自己的属性,就返回
if (O.hasOwnProperty(P)) {
return OP;
}
// 否则,继续分析原型
var __proto = O.[[Prototype]];
// 如果原型是null,返回undefined
// 这是可能的:最顶层Object.prototype.[[Prototype]]是null
if (__proto === null) {
return undefined;
}
// 否则,对原型链递归调用[[Get]],在各层的原型中查找属性
// 直到原型为null
return __proto.[[Get]](P)
请注意,因为[[Get]]在如下情况也会返回undefined:
The code copy is as follows:
if (window.someObject) {
...
}
这里,在window里没有找到someObject属性,然后会在原型里找,原型的原型里找,以此类推,如果都找不到,按照定义就返回undefined。
注意:in操作符也可以负责查找属性(也会查找原型链):
The code copy is as follows:
if ('someObject' in window) {
...
}
这有助于避免一些特殊问题:比如即便someObject存在,在someObject等于false的时候,第一轮检测就通不过。
[[Put]]方法
[[Put]]方法可以创建、更新对象自身的属性,并且掩盖原型里的同名属性。
O.[[Put]](P, V):
The code copy is as follows:
// 如果不能给属性写值,就退出
if (!O.[[CanPut]](P)) {
return;
}
// 如果对象没有自身的属性,就创建它
// 所有的attributes特性都是false
if (!O.hasOwnProperty(P)) {
createNewProperty(O, P, attributes: {
ReadOnly: false,
DontEnum: false,
DontDelete: false,
Internal: false
});
}
// 如果属性存在就设置值,但不改变attributes特性
OP = V
return;
For example:
The code copy is as follows:
Object.prototype.x = 100;
var foo = {};
console.log(foo.x); // 100, 继承属性
foo.x = 10; // [[Put]]
console.log(foo.x); // 10, 自身属性
delete foo.x;
console.log(foo.x); // 重新是100,继承属性
请注意,不能掩盖原型里的只读属性,赋值结果将忽略,这是由内部方法[[CanPut]]控制的。
// 例如,属性length是只读的,我们来掩盖一下length试试
function SuperString() {
/* nothing */
}
SuperString.prototype = new String("abc");
var foo = new SuperString();
console.log(foo.length); // 3, "abc"的长度
// 尝试掩盖
foo.length = 5;
console.log(foo.length); // 依然是3
但在ES5的严格模式下,如果掩盖只读属性的话,会保存TypeError错误。
属性访问器
内部方法[[Get]]和[[Put]]在ECMAScript里是通过点符号或者索引法来激活的,如果属性标示符是合法的名字的话,可以通过“.”来访问,而索引方运行动态定义名称。
The code copy is as follows:
var a = {testProperty: 10};
alert(a.testProperty); // 10, 点
alert(a['testProperty']); // 10, 索引
var propertyName = 'Property';
alert(a['test' + propertyName]); // 10, 动态属性通过索引的方式
这里有一个非常重要的特性――属性访问器总是使用ToObject规范来对待“.”左边的值。这种隐式转化和这句“在JavaScript中一切都是对象”有关系,(然而,当我们已经知道了,JavaScript里不是所有的值都是对象)。
如果对原始值进行属性访问器取值,访问之前会先对原始值进行对象包装(包括原始值),然后通过包装的对象进行访问属性,属性访问以后,包装对象就会被删除。
For example:
The code copy is as follows:
var a = 10; // 原始值
// 但是可以访问方法(就像对象一样)
alert(a.toString()); // "10"
// 此外,我们可以在a上创建一个心属性
a.test = 100; // 好像是没问题的
// 但,[[Get]]方法没有返回该属性的值,返回的却是undefined
alert(a.test); // undefined
那么,为什么整个例子里的原始值可以访问toString方法,而不能访问新创建的test属性呢?
答案很简单:
首先,正如我们所说,使用属性访问器以后,它已经不是原始值了,而是一个包装过的中间对象(整个例子是使用new Number(a)),而toString方法这时候是通过原型链查找到的:
The code copy is as follows:
// 执行a.toString()的原理:
1. wrapper = new Number(a);
2. wrapper.toString(); // "10"
3. delete wrapper;
接下来,[[Put]]方法创建新属性时候,也是通过包装装的对象进行的:
The code copy is as follows:
// 执行a.test = 100的原理:
1. wrapper = new Number(a);
2. wrapper.test = 100;
3. delete wrapper;
我们看到,在第3步的时候,包装的对象以及删除了,随着新创建的属性页被删除了――删除包装对象本身。
然后使用[[Get]]获取test值的时候,再一次创建了包装对象,但这时候包装的对象已经没有test属性了,所以返回的是undefined:
The code copy is as follows:
// 执行a.test的原理:
1. wrapper = new Number(a);
2. wrapper.test; // undefined
这种方式解释了原始值的读取方式,另外,任何原始值如果经常用在访问属性的话,时间效率考虑,都是直接用一个对象替代它;与此相反,如果不经常访问,或者只是用于计算的话,到可以保留这种形式。
inherit
我们知道,ECMAScript是使用基于原型的委托式继承。链和原型在原型链里已经提到过了。其实,所有委托的实现和原型链的查找分析都浓缩到[[Get]]方法了。
如果你完全理解[[Get]]方法,那JavaScript中的继承这个问题将不解自答了。
经常在论坛上谈论JavaScript中的继承时,我都是用一行代码来展示,事实上,我们不需要创建任何对象或函数,因为该语言已经是基于继承的了,代码如下:
The code copy is as follows:
alert(1..toString()); // "1"
我们已经知道了[[Get]]方法和属性访问器的原理了,我们来看看都发生了什么:
1.首先,从原始值1,通过new Number(1)创建包装对象
2.然后toString方法是从这个包装对象上继承得到的
为什么是继承的? 因为在ECMAScript中的对象可以有自己的属性,包装对象在这种情况下没有toString方法。 因此它是从原理里继承的,即Number.prototype。
注意有个微妙的地方,在上面的例子中的两个点不是一个错误。第一点是代表小数部分,第二个才是一个属性访问器:
The code copy is as follows:
1.toString(); // 语法错误!
(1).toString(); // OK
1..toString(); // OK
1['toString'](); // OK
原型链
让我们展示如何为用户定义对象创建原型链,非常简单:
The code copy is as follows:
function A() {
alert('A.[[Call]] activated');
this.x = 10;
}
A.prototype.y = 20;
var a = new A();
alert([ax, ay]); // 10 (自身), 20 (继承)
function B() {}
// 最近的原型链方式就是设置对象的原型为另外一个新对象
B.prototype = new A();
// 修复原型的constructor属性,否则的话是A了
B.prototype.constructor = B;
var b = new B();
alert([bx, by]); // 10, 20, 2个都是继承的
// [[Get]] bx:
// bx (no) -->
// b.[[Prototype]].x (yes) - 10
// [[Get]] by
// by (no) -->
// b.[[Prototype]].y (no) -->
// b.[[Prototype]].[[Prototype]].y (yes) - 20
// where b.[[Prototype]] === B.prototype,
// and b.[[Prototype]].[[Prototype]] === A.prototype
这种方法有两个特性:
首先,B.prototype将包含x属性。乍一看这可能不对,你可能会想x属性是在A里定义的并且B构造函数也是这样期望的。尽管原型继承正常情况是没问题的,但B构造函数有时候可能不需要x属性,与基于class的继承相比,所有的属性都复制到后代子类里了。
尽管如此,如果有需要(模拟基于类的继承)将x属性赋给B构造函数创建的对象上,有一些方法,我们后来来展示其中一种方式。
其次,这不是一个特征而是缺点――子类原型创建的时候,构造函数的代码也执行了,我们可以看到消息"A.[[Call]] activated"显示了两次――当用A构造函数创建对象赋给B.prototype属性的时候,另外一场是a对象创建自身的时候!
下面的例子比较关键,在父类的构造函数抛出的异常:可能实际对象创建的时候需要检查吧,但很明显,同样的case,也就是就是使用这些父对象作为原型的时候就会出错。
The code copy is as follows:
function A(param) {
if (!param) {
throw 'Param required';
}
this.param = param;
}
A.prototype.x = 10;
var a = new A(20);
alert([ax, a.param]); // 10, 20
function B() {}
B.prototype = new A(); // Error
此外,在父类的构造函数有太多代码的话也是一种缺点。
解决这些“功能”和问题,程序员使用原型链的标准模式(下面展示),主要目的就是在中间包装构造函数的创建,这些包装构造函数的链里包含需要的原型。
The code copy is as follows:
function A() {
alert('A.[[Call]] activated');
this.x = 10;
}
A.prototype.y = 20;
var a = new A();
alert([ax, ay]); // 10 (自身), 20 (集成)
function B() {
// 或者使用A.apply(this, arguments)
B.superproto.constructor.apply(this, arguments);
}
// 继承:通过空的中间构造函数将原型连在一起
var F = function () {};
F.prototype = A.prototype; // 引用
B.prototype = new F();
B.superproto = A.prototype; // 显示引用到另外一个原型上, "sugar"
// 修复原型的constructor属性,否则的就是A了
B.prototype.constructor = B;
var b = new B();
alert([bx, by]); // 10 (自身), 20 (集成)
注意,我们在b实例上创建了自己的x属性,通过B.superproto.constructor调用父构造函数来引用新创建对象的上下文。
我们也修复了父构造函数在创建子原型的时候不需要的调用,此时,消息"A.[[Call]] activated"在需要的时候才会显示。
为了在原型链里重复相同的行为(中间构造函数创建,设置superproto,恢复原始构造函数),下面的模板可以封装成一个非常方面的工具函数,其目的是连接原型的时候不是根据构造函数的实际名称。
The code copy is as follows:
function inherit(child, parent) {
var F = function () {};
F.prototype = parent.prototype
child.prototype = new F();
child.prototype.constructor = child;
child.superproto = parent.prototype;
return child;
}
因此,继承:
The code copy is as follows:
function A() {}
A.prototype.x = 10;
function B() {}
inherit(B, A); // 连接原型
var b = new B();
alert(bx); // 10, 在A.prototype查找到
也有很多语法形式(包装而成),但所有的语法行都是为了减少上述代码里的行为。
例如,如果我们把中间的构造函数放到外面,就可以优化前面的代码(因此,只有一个函数被创建),然后重用它:
The code copy is as follows:
var inherit = (function(){
function F() {}
return function (child, parent) {
F.prototype = parent.prototype;
child.prototype = new F;
child.prototype.constructor = child;
child.superproto = parent.prototype;
return child;
};
})();
由于对象的真实原型是[[Prototype]]属性,这意味着F.prototype可以很容易修改和重用,因为通过new F创建的child.prototype可以从child.prototype的当前值里获取[[Prototype]]:
The code copy is as follows:
function A() {}
A.prototype.x = 10;
function B() {}
inherit(B, A);
B.prototype.y = 20;
B.prototype.foo = function () {
alert("B#foo");
};
var b = new B();
alert(bx); // 10, 在A.prototype里查到
function C() {}
inherit(C, B);
// 使用"superproto"语法糖
// 调用父原型的同名方法
C.ptototype.foo = function () {
C.superproto.foo.call(this);
alert("C#foo");
};
var c = new C();
alert([cx, cy]); // 10, 20
c.foo(); // B#foo, C#foo
注意,ES5为原型链标准化了这个工具函数,那就是Object.create方法。ES3可以使用以下方式实现:
The code copy is as follows:
Object.create ||
Object.create = function (parent, properties) {
function F() {}
F.prototype = parent;
var child = new F;
for (var k in properties) {
child[k] = properties[k].value;
}
return child;
}
// Usage
var foo = {x: 10};
var bar = Object.create(foo, {y: {value: 20}});
console.log(bar.x, bar.y); // 10, 20
此外,所有模仿现在基于类的经典继承方式都是根据这个原则实现的,现在可以看到,它实际上不是基于类的继承,而是连接原型的一个很方便的代码重用。
in conclusion
本章内容已经很充分和详细了,希望这些资料对你有用,并且消除你对ECMAScript的疑问,如果你有任何问题,请留言,我们一起讨论。