JavaScript
运行在浏览器,需要浏览器中的解析器解析之后次啊能运行的脚本语言。
JavaScript最初受Java启发而开始设计,目的之一就是”看上去像Java”,因此语法上有类似之处,一些名称和命名规范也借自Java。
JavaScript于Java名称上近似,是当时Netscape为了营销考虑与Sun微系统达成协议的结果。
三部分组成
- ECMAScript
- DOM (Document Object Model)
- BOM (Browser Object Model)
JavaScript引擎
JavaScript的解释器被称为JavaScript引擎。
浏览器的一部分,广泛用于客户端,最早是在HTML网页上使用,用来给HTML网页增加动态功能。
JavaScript的应用
- 网页特效
- 服务端开发(Node.js)
- 命令行工具 (Node.js)
- 桌面程序 (Electron)
- App (Cordova、ReactNative)
- 控制硬件-物联网(Ruff)
- 游戏开发(cocos2d-js)
书写位置
-
内嵌式 (内嵌在head或body内)
1 2 3
<script> alert("Hello World!"); </script>
-
外链式
1
<script src="scripts/util.js"></script>
-
行内式
1 2 3
<button onclick="alert('Hello!')"> Click me! </button>
操作符
-
Strict equality (===)
-
Not(!)
-
Does-not-equal (!==)
变量声明
-
var
Declares a variable, optionally initializing it to a value.
This keyword can be used to declare both
local
andglobal
variables, depending on the execution context.1
var x = 12;
-
let
Declares a block-scoped, local variable, optionally initializing it to a value.
1
let y = 12;
-
const
Declares a block-scoped, read-only named constant.
Variable scope
let
andconst
declarations are scoped to theblock statement
that they are declared in.1 2 3 4 5
if (Math.random() > 0.5) { // Declaration of a scoped read-only constant (scoped to the if block) const y = 5; } console.log(y);
However, variables created with
var
are not blocked-scoped, but only local to the function (or global scope) that the block resides in.1 2 3 4 5 6 7 8 9 10 11
function test1() { var vb = "vert"; if (true) { // Declaration of a local variable (local to the test1 function that the block resides in.) var vc = "bert"; // Declaration of a scoped variable (scoped to the if block) let vd = "lettie"; } console.log(vc); console.log(vd);// unresolved variable or type vd }
-
global
When you declare a variable outside of any function, it is called a
global
variable, because it is available to any other code in the current document. -
local
When you declare a variable within a function, it is called a
local
variable, because it is available only within that function.
Block statement
The mose basic statement is a block statement, which is used to group statements.
The block is delimited by a pair of curly brackets
:
1
2
3
4
5
6
{
statement1;
statement2;
// ...
statementN;
}
Variable hoisting
Another unusual thing about variables in JavaScript is that you can refer to a variable declared later, with getting an exception. This concept is known as hoisting.
However, variables that are hoisted return a value of undefined
. So even if you declare and initialize after you use or refer to this variable, it still returns undefined
.
1
2
3
4
5
6
// Decalaring a anonymous function and immediately call it.
(function() {
// refer to variable declared later
console.log(myvar); // undefined
var myvar = 'local value';
})();
操作DOM
创建元素
1
document.createElement("button")
添加元素
1
document.body.append(resetButton);
移除元素
1
resetButton.parentNode.removeChild(resetButton);
获取元素
1
document.querySelector(".guessField");
获取input
的value
1
guessField.value
Primitive
Most of the time, a primitive value is represented directly at the lowest level of the language implementation.
All primitives are immutable; that is they cannot be altered. It is important not to confuse a primitive itself with a variable assigned a primitive value. The variable may be reassigned to a new value, but the existing value can not be changed in the ways that objects, arrays, and functions can be altered. The language does not offer utilities to mutate primitive values.
Wrapper
Primitives have no methods but still behave as if they do. When properties are accessed on primitives, JavaScript auto-boxes the value into a wrapper object and accesses the property on that object instead.
7 primitive data types
Primitive Wrapper Object
Number
-
Example
1 2 3
0b101; // 二进制 (binary) 0o13; //八进制 (octal) 0x0A; // 16进制 (hexadecimal)
-
A
primitive wrapper object
used to represent and manipulate numbers lkie37
or-9.25
-
A number literal like
37
in JavaScript code is afloating-point
value, not an integer. There’s no seperate integer type in common everyday use. -
Converting to number data types
1 2
let myNumber = "74"; myNumber = Number(myNumber) + 3;
-
toFixed
Round number to a fixed number of decimal places
1 2 3
const lotsOfDecimal = 1.766584958675746364; // 保留两位小数 const twoDecimalPlaces = lostsOfDecimal.toFixed(2); // 1.77
-
NaN
1 2
Number("abc") // NaN Number(undefined) // Nan
String
BigInt
Boolean
Symbol
Global_Objects(Standard built-in objects)
Value Properties
These global properties return a simple value. They have no properties or methods.
Function properties
These global functions-functions which are called globally, rather than on an object—-directly return their results to the caller.
eval()
isFinite()
isNaN()
parseFloat()
parseInt()
encodeURI()
encodeURIComponent()
decodeURI()
decodeURIComponent()
- Deprecated
escape()
Deprecatedunescape()
Deprecated
Fundamental objects
These are the fundamental, basic objects upon which all other objects are based. This includes general objects, booleans, functions, and symbols.
Error objects
Error objects are a special type of fundamental object. They include the basic Error
type, as well as several specialized error types.
Error
AggregateError
EvalError
InternalError
Non-standardRangeError
ReferenceError
SyntaxError
TypeError
URIError
Numbers and dates
These are the base objects representing numbers, dates, and mathematical calculations.
Text processing
These objects represent strings and support manipulating them.
Indexed collections
These objects represent collections of data which are ordered by an index value. This includes (typed) arrays and array-like constructs.
Array
Int8Array
Uint8Array
Uint8ClampedArray
Int16Array
Uint16Array
Int32Array
Uint32Array
Float32Array
Float64Array
BigInt64Array
BigUint64Array
Keyed collections
These objects represent collections which use keys. The iterable collections (Map
and Set
) contain elements which are easily iterated in the order of insertion.
Structured data
These objects represent and interact with structured data buffers and data coded using JavaScript Object Notation (JSON).
Control abstraction objects
Control abstractions can help to structure code, especially async code (without using deeply nested callbacks, for example).
Reflection
Internationalization
Additions to the ECMAScript core for language-sensitive functionalities.
Intl
Intl.Collator
Intl.DateTimeFormat
Intl.ListFormat
Intl.NumberFormat
Intl.PluralRules
Intl.RelativeTimeFormat
Intl.Locale
Working with objects
JavaScript is designed on a simple object-based paradigm.
An object is a collection of properties, and a property is an association between a name (or key) and a value.
A property’s value can be a function, in which case the property is known as a method.
Objects in JavaScript, just as in many other programming languages, can be compared to objects in real life. The concept of objects in JavaScript can be understood with real life, tangible objects.
In JavaScript, an object is a standalone entity, with properties and type.
object initializer
A comma-delimited list of zero or more pairs of names and associated values of an object, enclosed in curly braces.
1
2
3
4
5
const myCar = {
make: 'Ford',
model: 'Mustang',
year: 1969
};
等价于
1
2
3
4
const myCar = new Object();
myCar.make = 'Ford';
myCar.model = 'Mustang';
myCar.year = 1969;
Unassigned properties of an object are undefined
(and not null
).
1
myCar.color; // undefined
- bracket notation
1
2
3
myCar['make'] = 'Ford';
myCar['model'] = 'Mustang';
myCar['year'] = 1969;
An object property name can be any valid JavaScript string, or anything that can be converted to a string, including an empty string. However, any property name that is not a valid JavaScript identifier cannot use dot notation. For example, a property name that has a space or a hyphen, that starts with a number, or that is held inside a variable can only be accessed using the square bracket notation. This notation is also very useful when property names are to be dynamically determined, i.e. not determinable until runtime.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// four variables are created and assigned in a single go,
// separated by commas
const myObj = {},
str = 'myString',
rand = Math.random(),
anotherObj = {};
// Now, creating additional properties.
myObj.type = 'Dot syntax for a key named type';
myObj['date created'] = 'This key has a space';
myObj[str] = 'This key is in variable str';
myObj[rand] = 'A random number is the key here';
myObj[anotherObj] = 'This key is object anotherObj';
myObj[''] = 'This key is an empty string';
console.log(myObj);
console.log(myObj.myString);
/*
[Log] Object
: "This key is an empty string"
0.8916485437228595: "A random number is the key here"
[object Object]: "This key is object anotherObj"
date created: "This key has a space"
myString: "This key is in variable str"
type: "Dot syntax for a key named type"
*/
// notice that in the log, the order of the properties listed is not the same as the order they were created.
// [Log] This key is in variable str
propery accessors
Property accessors provide access to an object’s properties by using the dot notation or the bracket notation.
property names
Property names are string or Symbol. Any other value, including a number, is coerced to a string.
1
2
3
const object = {};
object["1"] = "value";
console.log(object[1]); // value
1
2
3
4
5
6
const foo = { uniqueProp: 1 };
const bar = { uniqueProp: 2 };
const object = {};
object[foo] = "value";
// This also outputs 'value', since both foo and bar are converted to the same string.
console.log(object[bar]);// value
Bracket notation vs. eval()
1
const x = eval(`document.forms.form_name.elements.${strFormControl}.value`);
eval()
is slow and should be avoided whenever possible. Also, strFormControl
would have to hold an identifier, which is not required for names and id
s of form controls. It is better to use bracket notation instead:
1
const x = document.forms.form_name.elements[strFormControl].value;
eval
The eval()
function evaluates JavaScript code represented as a string and returns its completion value. The source is parsed as a script.
method binding (this)
A function’s this
keyword behaves a little differently in JavaScript compared to other languages. It also has some differences between strict mode and non-strict mode.
In most cases, the value of this
is determined by how a function is called (runtime binding). It can’t be set by assignment during execution, and it may be different each time the function is called.
The bind()
method can set the value of a function’s this
regardless of how it’s called, and arrow functions don’t provide their own this
binding (it retains the this
value of the enclosing lexical context).
Global context
In the global execution context (outside of any function), this
refers to the global object whether in strict mode or not.
1
2
3
4
5
6
7
8
9
// In web browsers, the window object is also the global object:
console.log(this === window); // true
a = 37;
console.log(window.a); // 37
this.b = "MDN";
console.log(window.b) // "MDN"
console.log(b) // "MDN"
You can always easily get the global object using the global globalThis
property, regardless of the current context in which your code is running.
Function context
Inside a function, the value of this
depends on how the function is called.
Since the following code is not in strict mode, and because the value of this
is not set by the call, this
will default to the global object, which is window
in a browser.
1
2
3
4
5
6
7
8
9
function f1() {
return this;
}
// In a browser:
f1() === window; // true
// In Node:
f1() === globalThis; // true
In strict mode, however, if the value of this
is not set when entering an execution context, it remains as undefined
, as shown in the following example:
1
2
3
4
5
6
function f2() {
'use strict'; // see strict mode
return this;
}
f2() === undefined; // true
Note: In this example, this
should be undefined
, because f2
was called directly and not as a method or property of an object (e.g. window.f2()
). This feature wasn’t implemented in some browsers when they first started to support strict mode. As a result, they incorrectly returned the window
object.
Class Context
The behavior of this
in classes and functions is similar, since classes are functions under the hood. But there are some differences and caveats.
Within a class constructor, this
is a regular object. All non-static methods within the class are added to the prototype of this
:
1
2
3
4
5
6
7
8
9
10
11
class Example {
constructor() {
const proto = Object.getPrototypeOf(this);
console.log(Object.getOwnPropertyNames(proto));
}
first(){}
second(){}
static third(){}
}
new Example(); // ['constructor', 'first', 'second']
Note: Static methods are not properties of this
. They are properties of the class itself.
Derived classes
Unlike base class constructors, derived constructors have no initial this
binding. Calling super()
creates a this
binding within the constructor and essentially has the effect of evaluating the following line of code, where Base is the inherited class:
1
this = new Base();
Derived classes must not return before calling super()
, unless they return an Object
or have no constructor at all.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {}
class Good extends Base {}
class AlsoGood extends Base {
constructor() {
// reurn an object
return {a: 5};
}
}
class Bad extends Base {
// without calling super also without return an object
// this is bad
constructor() {}
}
new Good();
new AlsoGood();
new Bad(); // ReferenceError
Examples
this in function context
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// An object can be passed as the first argument to call
// or apply and this will be bound to it.
const obj = { a: 'Custom' };
// Variables declared with var become properties of the global object.
var a = 'Global';
function whatsThis() {
return this.a; // The value of this is dependent on how the function is called
}
whatsThis(); // 'Global' as this in the function isn't set, so it defaults to the global/window object in non–strict mode
whatsThis.call(obj); // 'Custom' as this in the function is set to obj
whatsThis.apply(obj); // 'Custom' as this in the function is set to obj
this and object conversion
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function add(c, d) {
return this.a + this.b + c + d;
}
const o = { a: 1, b: 3 };
// The first parameter is the object to use as
// 'this', subsequent parameters are passed as
// arguments in the function call
add.call(o, 5, 7); // 16
// The first parameter is the object to use as
// 'this', the second is an array whose
// members are used as the arguments in the function call
add.apply(o, [10, 20]); // 34
Note that in non–strict mode, with call
and apply
, if the value passed as this
is not an object, an attempt will be made to convert it to an object. Values null
and undefined
become the global object. Primitives like 7
or 'foo'
will be converted to an Object using the related constructor, so the primitive number 7
is converted to an object as if by new Number(7)
and the string 'foo'
to an object as if by new String('foo')
, e.g.
1
2
3
4
5
6
7
function bar() {
console.log(Object.prototype.toString.call(this));
}
bar.call(7); // [object Number]
bar.call('foo'); // [object String]
bar.call(undefined); // [object global]
bind()
ECMAScript 5 introduced Function.prototype.bind()
. Calling f.bind(someObject)
creates a new function with the same body and scope as f
, but where this
occurs in the original function, in the new function it is permanently bound to the first argument of bind
, regardless of how the function is being used.
1
2
3
4
5
6
7
8
9
10
11
12
function f() {
return this.a;
}
const g = f.bind({ a: 'azerty' });
console.log(g()); // azerty
const h = g.bind({ a: 'yoo' }); // bind only works once!
console.log(h()); // azerty
const o = { a: 37, f, g, h };
console.log(o.a, o.f(), o.g(), o.h()); // 37,37, azerty, azerty
Arrow functions
In arrow functions, this
retains the value of the enclosing lexical context’s this
. In global code, it will be set to the global object:
1
2
3
const globalObject = this;
const foo = (() => this);
console.log(foo() === globalObject); // true
Note: If this
arg is passed to call
, bind
, or apply
on invocation of an arrow function it will be ignored. You can still prepend arguments to the call, but the first argument (thisArg
) should be set to null
.
1
2
3
4
5
6
7
8
9
10
// Call as a method of an object
const obj = { func: foo };
console.log(obj.func() === globalObject); // true
// Attempt to set this using call
console.log(foo.call(obj) === globalObject); // true
// Attempt to set this using bind
const boundFoo = foo.bind(obj);
console.log(boundFoo() === globalObject); // true
No matter what, foo
’s this
is set to what it was when it was created (in the example above, the global object). The same applies to arrow functions created inside other functions: their this
remains that of the enclosing lexical context.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Create obj with a method bar that returns a function that
// returns its this. The returned function is created as
// an arrow function, so its this is permanently bound to the
// this of its enclosing function. The value of bar can be set
// in the call, which in turn sets the value of the
// returned function.
// 注意这里的语法解释
// Note: the `bar()` syntax is equivalent to `bar: function ()`
// in this context
const obj = {
bar() {
const x = (() => this);
return x;
}
};
// Call bar as a method of obj, setting its this to obj
// Assign a reference to the returned function to fn
const fn = obj.bar();
// 这里bar中调用context的global object通过上面一句的调佣已经设置为obj
// Call fn without setting this, would normally default
// to the global object or undefined in strict mode
console.log(fn() === obj); // true
// 注意;注意;注意;这里重新引用(但还没有调用),再调用时,调用context就已经变了
// But caution if you reference the method of obj without calling it
const fn2 = obj.bar;
// Calling the arrow function's this from inside the bar method()
// will now return window, because it follows the this from fn2.
console.log(fn2()() === window); // true
In the above, the function (call it anonymous function A) assigned to obj.bar
returns another function (call it anonymous function B) that is created as an arrow function. As a result, function B’s this
is permanently set to the this
of obj.bar
(function A) when called. When the returned function (function B) is called, its this
will always be what it was set to initially. In the above code example, function B’s this
is set to function A’s this
which is obj
, so it remains set to obj
even when called in a manner that would normally set its this
to undefined
or the global object (or any other method as in the previous example in the global execution context).
As an object method
When a function is called as a method of an object, its this
is set to the object the method is called on.
1
2
3
4
5
6
7
8
const o = {
prop: 37,
f() {
return this.prop;
}
};
console.log(o.f()); // 37
Note that this behavior is not at all affected by how or where the function was defined. In the previous example, we defined the function inline as the f
member during the definition of o
. However, we could have just as easily defined the function first and later attached it to o.f
. Doing so results in the same behavior:
1
2
3
4
5
6
7
8
9
10
const o = { prop: 37 };
function independent() {
return this.prop;
}
o.f = independent;
console.log(o.f()); // 37
This demonstrates that it matters only that the function was invoked from the f
member of o
.
Similarly, the this
binding is only affected by the most immediate member reference. In the following example, when we invoke the function, we call it as a method g
of the object o.b
. This time during execution, this
inside the function will refer to o.b
. The fact that the object is itself a member of o
has no consequence; the most immediate reference is all that matters.
1
2
o.b = { g: independent, prop: 42 };
console.log(o.b.g()); // 42
this on the object’s prototype chain
The same notion holds true for methods defined somewhere on the object’s prototype chain. If the method is on an object’s prototype chain, this
refers to the object the method was called on, as if the method were on the object.
1
2
3
4
5
6
7
8
9
10
const o = {
f() {
return this.a + this.b;
},
};
const p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
In this example, the object assigned to the variable p
doesn’t have its own f
property, it inherits it from its prototype. But it doesn’t matter that the lookup for f
eventually finds a member with that name on o
; the lookup began as a reference to p.f
, so this
inside the function takes the value of the object referred to as p
. That is, since f
is called as a method of p
, its this
refers to p
. This is an interesting feature of JavaScript’s prototype inheritance.
this with getter or setter
Again, the same notion holds true when a function is invoked from a getter or a setter. A function used as getter or setter has its this
bound to the object from which the property is being set or gotten.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function sum() {
return this.a + this.b + this.c;
}
const o = {
a: 1,
b: 2,
c: 3,
get average() {
return (this.a + this.b + this.c) / 3;
}
};
Object.defineProperty(o, 'sum', {
get: sum,
enumerable: true,
configurable: true,
});
console.log(o.average, o.sum); // 2, 6
As a constructor
When a function is used as a constructor (with the new
keyword), its this
is bound to the new object being constructed.
Note: While the default for a constructor is to return the object referenced by this
, it can instead return some other object (if the return value isn’t an object, then the this
object is returned).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* Constructors work like this:
*
* function MyConstructor() {
* // Actual function body code goes here.
* // Create properties on `this` as
* // desired by assigning to them, for example,
* this.fum = "nom";
* // et cetera...
*
* // If the function has a return statement that
* // returns an object, that object will be the
* // result of the `new` expression. Otherwise,
* // the result of the expression is the object
* // currently bound to `this`
* // (i.e., the common case most usually seen).
* }
*/
function C() {
this.a = 37;
}
let o = new C();
console.log(o.a); // 37
function C2() {
this.a = 37;
// 并不返回this,而是一个别的对象
return { a: 38 };
}
o = new C2();
console.log(o.a); // 38
In the last example (C2
), because an object was returned during construction, the new object that this
was bound to gets discarded. (This essentially makes the statement this.a = 37;
dead code. It’s not exactly dead because it gets executed, but it can be eliminated with no outside effects.)
As a DOM event handler
When a function is used as an event handler, its this
is set to the element on which the listener is placed (some browsers do not follow this convention for listeners added dynamically with methods other than addEventListener()
).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// When called as a listener, turns the related element blue
function bluify(e) {
// Always true
console.log(this === e.currentTarget);
// true when currentTarget and target are the same object
console.log(this === e.target);
this.style.backgroundColor = '#A5D9F3';
}
// 注意这里的getElementsByTagName传参
// tag指的是标签或与元素, tagName指的是标签名称
// Get a list of every element in the document
const elements = document.getElementsByTagName('*');
// Add bluify as a click listener so when the
// element is clicked on, it turns blue
for (const element of elements) {
element.addEventListener('click', bluify, false);
}
In an inline event handler
When the code is called from an inline on-event handler, its this
is set to the DOM element on which the listener is placed:
1
2
3
<button onclick="alert(this.tagName.toLowerCase());">
Show this
</button>
The above alert shows button
. Note however that only the outer code has its this
set this way:
1
2
3
<button onclick="alert((function () { return this; })());">
Show inner this
</button>
In this case, the inner function’s this
isn’t set so it returns the global/window object (i.e. the default object in non–strict mode where this
isn’t set by the call).
this in classes
Just like with regular functions, the value of this
within methods depends on how they are called. Sometimes it is useful to override this behavior so that this
within classes always refers to the class instance. To achieve this, bind the class methods in the constructor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Car {
constructor() {
// Bind sayBye but not sayHi to show the difference
this.sayBye = this.sayBye.bind(this);
}
sayHi() {
console.log(`Hello from ${this.name}`);
}
sayBye() {
console.log(`Bye from ${this.name}`);
}
get name() {
return 'Ferrari';
}
}
class Bird {
get name() {
return 'Tweety';
}
}
const car = new Car();
const bird = new Bird();
// The value of 'this' in methods depends on their caller
car.sayHi(); // Hello from Ferrari
bird.sayHi = car.sayHi;
bird.sayHi(); // Hello from Tweety
// For bound methods, 'this' doesn't depend on the caller
bird.sayBye = car.sayBye;
bird.sayBye(); // Bye from Ferrari
Note: Classes are always strict mode code. Calling methods with an undefined this
will throw an error.
strict mode
Strict mode makes several changes to normal JavaScript semantics:
- Eliminates some JavaScript silent errors by changing them to throw errors.
- Fixes mistakes that make it difficult for JavaScript engines to perform optimizations: strict mode code can sometimes be made to run faster than identical code that’s not strict mode.
- Prohibits some syntax likely to be defined in future versions of ECMAScript.
stric mode for scripts
1
2
3
// Whole-script strict mode syntax
'use strict';
const v = "Hi! I'm a strict mode script!";
strict mode for functions
1
2
3
4
5
6
7
8
9
10
11
function myStrictFunction() {
// Function-level strict mode syntax
'use strict';
function nested() {
return 'And so am I!';
}
return `Hi! I'm a strict mode function! ${nested()}`;
}
function myNotStrictFunction() {
return "I'm not strict.";
}
strict mode for modules
The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
strict mode for classes
All parts of ECMAScript classes are strict mode code, including both class declarations and class expressions — and so also including all parts of class bodies.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C1 {
// All code here is evaluated in strict mode
test() {
delete Object.prototype;
}
}
new C1().test(); // TypeError, because test() is in strict mode
const C2 = class {
// All code here is evaluated in strict mode
};
// Code here may not be in strict mode
delete Object.prototype; // Will not throw error
arrow function
- Arrow functions don’t have their own bindings to
this
,arguments
orsuper
, and should not be used as methods. - Arrow functions don’t have access to the
new.target
keyword. - Arrow functions aren’t suitable for
call
,apply
andbind
methods, which generally rely on establishing a scope. - Arrow functions cannot be used as constructors.
- Arrow functions cannot use
yield
, within its body.
1
2
3
4
5
6
7
8
const materials = [
'Hydrogen',
'Helium',
'Lithium',
'Beryllium'
];
// arrow function
console.log(materials.map(material => material.length));
comparing traditional functions to arrow functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Traditional Anonymous Function
(function (a) {
return a + 100;
});
// Arrow Function Break Down
// 1. Remove the word "function" and place arrow between the argument and opening body bracket
(a) => {
return a + 100;
};
// 2. Remove the body braces and word "return" — the return is implied.
(a) => a + 100;
// 3. Remove the argument parentheses
a => a + 100;
The { braces } and ( parentheses ) and “return” are required in some cases.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Traditional Anonymous Function
(function (a, b) {
return a + b + 100;
});
// Arrow Function
(a, b) => a + b + 100;
const a = 4;
const b = 2;
// Traditional Anonymous Function (no arguments)
(function() {
return a + b + 100;
});
// Arrow Function (no arguments)
() => a + b + 100;
1
2
3
4
5
6
7
8
9
10
11
// Traditional Anonymous Function
(function (a, b) {
const chuck = 42;
return a + b + chuck;
});
// Arrow Function
(a, b) => {
const chuck = 42;
return a + b + chuck;
};
And finally, for named functions we treat arrow expressions like variables:
1
2
3
4
5
6
7
// Traditional Function
function bob(a) {
return a + 100;
}
// Arrow Function
const bob2 = (a) => a + 100;
Inheritance and the prototype chain
JavaScript is a bit confusing for developers experienced in class-based languages (like Java or C++), as it is dynamic and does not have static types.
When it comes to inheritance, JavaScript only has one construct: objects. Each object has a private property which holds a link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null
as its prototype.
By definition, null
has no prototype, and acts as the final link in this prototype chain.
It is possible to mutate any member of the prototype chain or even swap out the prototype at runtime, so concepts like static dispatching do not exist in JavaScript.
While this confusion is often considered to be one of JavaScript’s weaknesses, the prototypical inheritance model itself is, in fact, more powerful than the classic model. It is, for example, fairly trivial to build a classic model on top of a prototypical model — which is how classes are implemented.
Although classes are now widely adopted and have become a new paradigm in JavaScript, classes do not bring a new inheritance pattern. While classes abstract most of the prototypical mechanism away, understanding how prototypes work under the hood is still useful.
Inheriting properties
JavaScript objects are dynamic “bags” of properties (referred to as own properties). JavaScript objects have a link to a prototype object. When trying to access a property of an object, the property will not only be sought on the object but on the prototype of the object, the prototype of the prototype, and so on until either a property with a matching name is found or the end of the prototype chain is reached.
[[Prototype]]
Note: Following the ECMAScript standard, the notation someObject.[[Prototype]]
is used to designate the prototype of someObject
. The [[Prototype]]
internal slot can be accessed with the Object.getPrototypeOf()
and Object.setPrototypeOf()
functions. This is equivalent to the JavaScript accessor __proto__
which is non-standard but de-facto implemented by many JavaScript engines. To prevent confusion while keeping it succinct, in our notation we will avoid using obj.__proto__
but use obj.[[Prototype]]
instead. This corresponds to Object.getPrototypeOf(obj)
.
It should not be confused with the func.prototype
property of functions, which instead specifies the [[Prototype]]
to be assigned to all instances of objects created by the given function when used as a constructor. We will discuss the prototype
property of constructor functions in a later section.
There are several ways to specify the [[Prototype]]
of an object, which are listed in a later section. For now, we will use the __proto__
syntax for illustration. It’s worth noting that the { __proto__: ... }
syntax is different from the obj.__proto__
accessor: the former is standard and not deprecated.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const o = {
a: 1,
b: 2,
// __proto__ sets the [[Prototype]]. It's specified here
// as another object literal.
__proto__: {
b: 3,
c: 4,
},
};
// o.[[Prototype]] has properties b and c.
// o.[[Prototype]].[[Prototype]] is Object.prototype (we will explain
// what that means later).
// Finally, o.[[Prototype]].[[Prototype]].[[Prototype]] is null.
// This is the end of the prototype chain, as null,
// by definition, has no [[Prototype]].
// Thus, the full prototype chain looks like:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null
console.log(o.a); // 1
// Is there an 'a' own property on o? Yes, and its value is 1.
console.log(o.b); // 2
// Is there a 'b' own property on o? Yes, and its value is 2.
// The prototype also has a 'b' property, but it's not visited.
// This is called Property Shadowing
console.log(o.c); // 4
// Is there a 'c' own property on o? No, check its prototype.
// Is there a 'c' own property on o.[[Prototype]]? Yes, its value is 4.
console.log(o.d); // undefined
// 注意这里沿着prototype chain找property的分析
// Is there a 'd' own property on o? No, check its prototype.
// Is there a 'd' own property on o.[[Prototype]]? No, check its prototype.
// o.[[Prototype]].[[Prototype]] is Object.prototype and
// there is no 'd' property by default, check its prototype.
// o.[[Prototype]].[[Prototype]].[[Prototype]] is null, stop searching,
// no property found, return undefined.
Setting a property to an object creates an own property. The only exception to the getting and setting behavior rules is when it’s intercepted by a getter or setter.
Similarly, you can create longer prototype chains, and a property will be sought on all of them.
Inheriting methods
JavaScript does not have “methods” in the form that class-based languages define them. In JavaScript, any function can be added to an object in the form of a property. An inherited function acts just as any other property, including property shadowing as shown above (in this case, a form of method overriding).
When an inherited function is executed, the value of this
points to the inheriting object, not to the prototype object where the function is an own property.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const parent = {
value: 2,
method() {
return this.value + 1;
}
};
console.log(parent.method()); // 3
// When calling parent.method in this case, 'this' refers to parent
// child is an object that inherits from parent
const child = {
__proto__: parent,
};
console.log(child.method()); // 3
// 注意这里this的分析,符合一般的认知,指向子对象,从子对象开始找
// When child.method is called, 'this' refers to child.
// So when child inherits the method of parent,
// The property 'value' is sought on child. However, since child
// doesn't have an own property called 'value', the property is
// found on the [[Prototype]], which is parent.value.
// 这里给child增加了一个property
// 注意这里的术语'Shadow'
child.value = 4; // assign the value 4 to the property 'value' on child.
// This shadows the 'value' property on parent.
// The child object now looks like:
// { value: 4, __proto__: { value: 2, method: [Function] } }
console.log(child.method()); // 5
// Since child now has the 'value' property, 'this.value' means
// child.value instead
Constructors
The power of prototypes is that we can reuse a set of properties if they should be present on every instance — especially for methods. Suppose we are to create a series of boxes, where each box is an object that contains a value which can be accessed through a getValue
function. A naïve implementation would be:
1
2
3
4
5
const boxes = [
{ value: 1, getValue() { return this.value; } },
{ value: 2, getValue() { return this.value; } },
{ value: 3, getValue() { return this.value; } },
];
This is subpar, because each instance has its own function property that does the same thing, which is redundant and unnecessary. Instead, we can move getValue
to the [[Prototype]]
of all boxes:
1
2
3
4
5
6
7
8
9
const boxPrototype = {
getValue() { return this.value; },
};
const boxes = [
{ value: 1, __proto__: boxPrototype },
{ value: 2, __proto__: boxPrototype },
{ value: 3, __proto__: boxPrototype },
];
This way, all boxes’ getValue
method will refer to the same function, lowering memory usage. However, manually binding the __proto__
for every object creation is still very inconvenient. This is when we would use a constructor function, which automatically sets the [[Prototype]]
for every object manufactured. Constructors are functions called with new
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// A constructor function
function Box(value) {
this.value = value;
}
// Properties all boxes created from the Box() constructor
// will have
Box.prototype.getValue = function () {
return this.value;
};
const boxes = [
new Box(1),
new Box(2),
new Box(3),
];
注意这段解释,可以发现这种基于prototype的实现与类有些相似(在方法继承方面),保证了方法代码只有一份(仅此而已): We say that new Box(1)
is an instance created from the Box
constructor function. Box.prototype
is not much different from the boxPrototype
object we created previously — it’s just a plain object. Every instance created from a constructor function will automatically have the constructor’s prototype
property as its [[Prototype]]
— that is, Object.getPrototypeOf(new Box()) === Box.prototype
. Constructor.prototype
by default has one own property: constructor
, which references the constructor function itself — that is, Box.prototype.constructor === Box
. This allows one to access the original constructor from any instance.
Note: If a non-primitive is returned from the constructor function, that value will become the result of the new
expression. In this case the [[Prototype]]
may not be correctly bound — but this should not happen much in practice.
The above constructor function can be rewritten in classes as:
1
2
3
4
5
6
7
8
9
10
class Box {
constructor(value) {
this.value = value;
}
// Methods are created on Box.prototype
getValue() {
return this.value;
}
}
Classes are syntax sugar over constructor functions, which means you can still manipulate Box.prototype
to change the behavior of all instances. However, because classes are designed to be an abstraction over the underlying prototype mechanism, we will use the more-lightweight constructor function syntax for this tutorial to fully demonstrate how prototypes work.
Because Box.prototype
references the same object as the [[Prototype]]
of all instances, we can change the behavior of all instances by mutating Box.prototype
.
1
2
3
4
5
6
7
8
9
10
11
12
13
function Box(value) {
this.value = value;
}
Box.prototype.getValue = function () {
return this.value;
};
const box = new Box(1);
// Mutate Box.prototype after an instance has already been created
Box.prototype.getValue = function () {
return this.value + 1;
};
box.getValue(); // 2
A corollary is, re-assigning Constructor.prototype
(Constructor.prototype = ...
) is a bad idea for two reasons:
- The
[[Prototype]]
of instances created before the reassignment is now referencing a different object from the[[Prototype]]
of instances created after the reassignment — mutating one’s[[Prototype]]
no longer mutates the other. (设置前后创建的实例自然是有不同的[[Prototype]]) - Unless you manually re-set the
constructor
property, the constructor function can no longer be traced frominstance.constructor
, which may break user expectation. Some built-in operations will read theconstructor
property as well, and if it is not set, they may not work as expected. (暂时理解为设置后的Box.prototype不能trace constructor)
Constructor.prototype
is only useful when constructing instances. It has nothing to do with Constructor.[[Prototype]]
, which is the constructor function’s own prototype, which is Function.prototype
— that is, Object.getPrototypeOf(Constructor) === Function.prototype
.
Implicit constructors of literals
Some literal syntaxes in JavaScript create instances that implicitly set the [[Prototype]]
. For example:
1
2
3
4
5
6
7
8
9
10
11
12
// Object literals (without the `__proto__` key) automatically
// have `Object.prototype` as their `[[Prototype]]`
const object = { a: 1 };
Object.getPrototypeOf(object) === Object.prototype; // true
// Array literals automatically have `Array.prototype` as their `[[Prototype]]`
const array = [1, 2, 3];
Object.getPrototypeOf(array) === Array.prototype; // true
// RegExp literals automatically have `RegExp.prototype` as their `[[Prototype]]`
const regexp = /abc/;
Object.getPrototypeOf(regexp) === RegExp.prototype; // true
We can “de-sugar” them into their constructor form.
1
2
const array = new Array(1, 2, 3);
const regexp = new RegExp("abc");
Warning: There is one misfeature that used to be prevalent — extending Object.prototype
or one of the other built-in prototypes. An example of this misfeature is, defining Array.prototype.myMethod = function () {...}
and then using myMethod
on all array instances.
This misfeature is called monkey patching. Doing monkey patching risks forward compatibility, because if the language adds this method in the future but with a different signature, your code will break. It has led to incidents like the SmooshGate, and can be a great nuisance for the language to advance since JavaScript tries to “not break the web”.
The only good reason for extending a built-in prototype is to backport the features of newer JavaScript engines, like Array.prototype.forEach
.
It may be interesting to note that due to historical reasons, some built-in constructors’ prototype
property are instances themselves. For example, Number.prototype
is a number 0, Array.prototype
is an empty array, and RegExp.prototype
is /(?:)/
.
1
2
3
4
5
Number.prototype + 1 // 1
Array.prototype.map((x) => x + 1) // []
String.prototype + "a" // "a"
RegExp.prototype.source // "(?:)"
Function.prototype() // Function.prototype is a no-op function by itself
However, this is not the case for user-defined constructors, nor for modern constructors like Map
.
1
2
Map.prototype.get(1)
// Uncaught TypeError: get method called on incompatible Map.prototype
Building longer inheritance chains
The Constructor.prototype
property will become the [[Prototype]]
of the constructor’s instances, as-is — including Constructor.prototype
’s own [[Prototype]]
. By default, Constructor.prototype
is a plain object — that is, Object.getPrototypeOf(Constructor.prototype) === Object.prototype
. The only exception is Object.prototype
itself, whose [[Prototype]]
is null
— that is, Object.getPrototypeOf(Object.prototype) === null
. Therefore, a typical constructor will build the following prototype chain:
1
2
3
4
5
function Constructor() {}
const obj = new Constructor();
// obj ---> Constructor.prototype ---> Object.prototype ---> null
To build longer prototype chains, we can set the [[Prototype]]
of Constructor.prototype
via the Object.setPrototypeOf()
function.
1
2
3
4
5
6
7
8
9
10
11
function Base() {}
function Derived() {}
// Set the `[[Prototype]]` of `Derived.prototype`
// to `Base.prototype`
Object.setPrototypeOf(
Derived.prototype,
Base.prototype,
);
const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
In class terms, this is equivalent to using the extends
syntax.
1
2
3
4
5
class Base {}
class Derived extends Base {}
const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
You may also see some legacy code using Object.create()
to build the inheritance chain. However, because this reassigns the prototype
property and removes the constructor
property, it can be more error-prone, while performance gains may not be apparent if the constructors haven’t created any instances yet. (照应了上面corollary)
1
2
3
4
5
6
function Base() {}
function Derived() {}
// Re-assigns `Derived.prototype` to a new object
// with `Base.prototype` as its `[[Prototype]]`
// DON'T DO THIS — use Object.setPrototypeOf to mutate it instead
Derived.prototype = Object.create(Base.prototype);
Inspecting prototypes: a deeper dive
In JavaScript, as mentioned above, functions are able to have properties. All functions have a special property named prototype
.
1
2
3
4
5
6
7
8
function doSomething() {}
console.log(doSomething.prototype);
// It does not matter how you declare the function; a
// function in JavaScript will always have a default
// prototype property — with one exception: an arrow
// function doesn't have a default prototype property:
const doSomethingFromArrowFunction = () => {};
console.log(doSomethingFromArrowFunction.prototype);
Object.prototype.constructor
The constructor
property returns a reference to the Object
constructor function that created the instance object. Note that the value of this property is a reference to the function itself, not a string containing the function’s name.
Any object (with the exception of null
prototype objects) will have a constructor
property on its [[Prototype]]
. Objects created with literals will also have a constructor
property that points to the constructor type for that object — for example, array literals create Array
objects, and object literals create plain objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const o1 = {};
o1.constructor === Object; // true
const o2 = new Object();
o2.constructor === Object; // true
const a1 = [];
a1.constructor === Array; // true
const a2 = new Array();
a2.constructor === Array; // true
const n = 3;
n.constructor === Number; // true
Note that constructor
usually comes from the constructor’s prototype
property. If you have a longer prototype chain, you can usually expect every object in the chain to have a constructor
property.
1
2
3
4
5
const o = new TypeError(); // Inheritance: TypeError -> Error -> Object
const proto = Object.getPrototypeOf;
proto(o).constructor === TypeError; // true
proto(proto(o)).constructor === Error; // true
proto(proto(proto(o))).constructor === Object; // true
Examples
Displaying the constructor of an object
1
2
3
4
5
6
function Tree(name) {
this.name = name;
}
const theTree = new Tree("Redwood");
console.log(`theTree.constructor is ${theTree.constructor}`);
Assigning the constructor property to an object
1
2
3
4
5
6
7
8
9
10
11
12
const arr = [];
arr.constructor = String;
arr.constructor === String; // true
// 注意这里
arr instanceof String; // false
arr instanceof Array; // true
const foo = new Foo();
foo.constructor = "bar";
foo.constructor === "bar"; // true
// etc.
This does not overwrite the old constructor
property — it was originally present on the instance’s [[Prototype]]
, not as its own property. (看来overwrite和shadow不是一回事)
1
2
3
4
5
6
const arr = [];
Object.hasOwn(arr, "constructor"); // false
Object.hasOwn(Object.getPrototypeOf(arr), "constructor"); // true
arr.constructor = String;
Object.hasOwn(arr, "constructor"); // true — the instance property shadows the one on its prototype
But even when Object.getPrototypeOf(a).constructor
is re-assigned, it won’t change other behaviors of the object. For example, the behavior of instanceof
is controlled by Symbol.hasInstance
, not constructor
:
1
2
3
4
const arr = [];
arr.constructor = String;
arr instanceof String; // false
arr instanceof Array; // true
There is nothing protecting the constructor
property from being re-assigned or shadowed, so using it to detect the type of a variable should usually be avoided in favor of less fragile ways like instanceof
and Symbol.toStringTag
for objects, or typeof
for primitives.
Chaning the constructor of a constructor function’s prototype
Every constructor has a
prototype
property, which will become the instance’s[[Prototype]]
when called via thenew
operator.
ConstructorFunction.prototype.constructor
will therefore become a property on the instance’s[[Prototype]]
, as previously demonstrated.However, if
ConstructorFunction.prototype
is re-assigned, theconstructor
property will be lost. For example, the following is a common way to create an inheritance pattern: (再次照应,其实很容易理解,所有实例的[[Prototype]]都将发生改变,instance.constructor也自然指向新的(本来就不是instance自己的嘛),旧的就访问不到了)
1 2 3 4 5 6 7 8 9 10 function Parent() { // … } Parent.prototype.parentMethod = function () {}; function Child() { Parent.call(this); // Make sure everything is initialized properly } // Pointing the [[Prototype]] of Child.prototype to Parent.prototype Child.prototype = Object.create(Parent.prototype);This is usually not a big deal — the language almost never reads the
constructor
property of an object. The only exception is when using@@species
to create new instances of a class, but such cases are rare, and you should be using theextends
syntax to subclass builtins anyway.However, ensuring that
Child.prototype.constructor
always points toChild
itself is crucial when some caller is usingconstructor
to access the original class from an instance. Take the following case: the object has thecreate()
method to create itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Parent() { // … } function CreatedConstructor() { Parent.call(this); } CreatedConstructor.prototype = Object.create(Parent.prototype); CreatedConstructor.prototype.create = function () { return new this.constructor(); }; new CreatedConstructor().create().create(); // TypeError: new CreatedConstructor().create().create is undefined, since constructor === Parentn the example above, an exception is thrown, since the
constructor
links toParent
. To avoid this, just assign the necessary constructor you are going to use.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function Parent() { // … } function CreatedConstructor() { // … } // 同时指定prototype和constructor CreatedConstructor.prototype = Object.create(Parent.prototype, { // Return original constructor to Child constructor: { value: CreatedConstructor, enumerable: false, // Make it non-enumerable, so it won't appear in `for...in` loop writable: true, configurable: true, }, }); CreatedConstructor.prototype.create = function () { return new this.constructor(); }; new CreatedConstructor().create().create(); // it's pretty fineNote that when manually adding the
constructor
property, it’s crucial to make the property non-enumerable, soconstructor
won’t be visited infor...in
loops — as it normally isn’t.If the code above looks like too much boilerplate, you may also consider using
Object.setPrototypeOf()
to manipulate the prototype chain.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Parent() { // … } function CreatedConstructor() { // … } Object.setPrototypeOf(CreatedConstructor.prototype, Parent.prototype); CreatedConstructor.prototype.create = function () { return new this.constructor(); }; new CreatedConstructor().create().create(); // still works without re-creating constructor property
Object.setPrototypeOf()
comes with its potential performance downsides because all previously created objects involved in the prototype chain have to be re-compiled; but if the above initialization code happens beforeParent
orCreatedConstructor
are constructed, the effect should be minimal.Let’s consider one more involved case.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function ParentWithStatic() {} ParentWithStatic.startPosition = { x: 0, y: 0 }; // Static member property ParentWithStatic.getStartPosition = function () { return this.startPosition; }; function Child(x, y) { this.position = { x, y }; } Child.prototype = Object.create(ParentWithStatic.prototype, { // Return original constructor to Child constructor: { value: Child, enumerable: false, writable: true, configurable: true, }, }); Child.prototype.getOffsetByInitialPosition = function () { const position = this.position; // Using this.constructor, in hope that getStartPosition exists as a static method const startPosition = this.constructor.getStartPosition(); return { offsetX: startPosition.x - position.x, offsetY: startPosition.y - position.y, }; }; new Child(1, 1).getOffsetByInitialPosition(); // Error: this.constructor.getStartPosition is undefined, since the // constructor is Child, which doesn't have the getStartPosition static methodFor this example to work properly, we can reassign the
Parent
’s static properties toChild
:
1 2 3 4 5 6 7 8 9 10 11 12 13 // … // 注意这里的assign,能把静态方法拿过来 Object.assign(Child, ParentWithStatic); // Notice that we assign it before we create() a prototype below Child.prototype = Object.create(ParentWithStatic.prototype, { // Return original constructor to Child constructor: { value: Child, enumerable: false, writable: true, configurable: true, }, }); // …But even better, we can make the constructor functions themselves extend each other, as classes’
extends
do.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function ParentWithStatic() {} ParentWithStatic.startPosition = { x: 0, y: 0 }; // Static member property ParentWithStatic.getStartPosition = function () { return this.startPosition; }; function Child(x, y) { this.position = { x, y }; } // Properly create inheritance! Object.setPrototypeOf(Child.prototype, ParentWithStatic.prototype); // 注意这个用法,实现了constructor的继承 Object.setPrototypeOf(Child, ParentWithStatic); Child.prototype.getOffsetByInitialPosition = function () { const position = this.position; const startPosition = this.constructor.getStartPosition(); return { offsetX: startPosition.x - position.x, offsetY: startPosition.y - position.y, }; }; console.log(new Child(1, 1).getOffsetByInitialPosition()); // { offsetX: -1, offsetY: -1 }Again, using
Object.setPrototypeOf()
may have adverse performance effects, so make sure it happens immediately after the constructor declaration and before any instances are created — to avoid objects being “tainted”. (注意性能影响)
Object.setPrototypeOf
1
2
3
4
5
6
7
8
9
10
11
12
class Human {}
class SuperHero {}
// Set the instance properties
Object.setPrototypeOf(SuperHero.prototype, Human.prototype);
// 注意这里Hook静态方法的方式
// Hook up the static properties
Object.setPrototypeOf(SuperHero, Human);
const superMan = new SuperHero();
Object.assign()
The Object.assign()
method copies all enumerable own properties from one or more source objects to a target object. It returns the modified target object. (照应了上面拷贝静态方法的用法)
Examples
Cloning an object (不是Deep Clone)
1
2
3
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
Warning for Deep Clone
For deep cloning, we need to use alternatives, because Object.assign()
copies property values.
If the source value is a reference to an object, it only copies the reference value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const obj1 = { a: 0, b: { c: 0 } };
const obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 0, b: { c: 0 } }
obj1.a = 1;
console.log(obj1); // { a: 1, b: { c: 0 } }
console.log(obj2); // { a: 0, b: { c: 0 } }
obj2.a = 2;
console.log(obj1); // { a: 1, b: { c: 0 } }
console.log(obj2); // { a: 2, b: { c: 0 } }
obj2.b.c = 3;
console.log(obj1); // { a: 1, b: { c: 3 } }
console.log(obj2); // { a: 2, b: { c: 3 } }
// 注意这里深拷贝对象的用法
// Deep Clone
const obj3 = { a: 0, b: { c: 0 } };
const obj4 = JSON.parse(JSON.stringify(obj3));
obj3.a = 4;
obj3.b.c = 4;
console.log(obj4); // { a: 0, b: { c: 0 } }
Merging objects
1
2
3
4
5
6
7
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };
const obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, target object itself is changed.
Merging objects with same properties
1
2
3
4
5
6
const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };
const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
The properties are overwritten by other objects that have the same properties later in the parameters order.
Copying symbol-typed properties
1
2
3
4
5
6
const o1 = { a: 1 };
const o2 = { [Symbol('foo')]: 2 };
const obj = Object.assign({}, o1, o2);
console.log(obj); // { a : 1, [Symbol("foo")]: 2 } (cf. bug 1207182 on Firefox)
Object.getOwnPropertySymbols(obj); // [Symbol(foo)]
Properties on the prototype chain and non-enumerable properties cannot be copied
1
2
3
4
5
6
7
8
9
10
11
12
const obj = Object.create({ foo: 1 }, { // foo is on obj's prototype chain.
bar: {
value: 2 // bar is a non-enumerable property.
},
baz: {
value: 3,
enumerable: true // baz is an own enumerable property.
}
});
const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }
Primitives will be wrapped to objects
1
2
3
4
5
6
7
8
9
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const v4 = Symbol('foo');
// 注意: null和undefined 会被忽略
const obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// Primitives will be wrapped, null and undefined will be ignored.
// Note, only string wrappers can have own enumerable properties.
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
Exceptions will interrupt the ongoing copying task
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const target = Object.defineProperty({}, 'foo', {
value: 1,
writable: false
}); // target.foo is a read-only property
Object.assign(target, { bar: 2 }, { foo2: 3, foo: 3, foo3: 3 }, { baz: 4 });
// TypeError: "foo" is read-only
// The Exception is thrown when assigning target.foo
console.log(target.bar); // 2, the first source was copied successfully.
console.log(target.foo2); // 3, the first property of the second source was copied successfully.
console.log(target.foo); // 1, exception is thrown here.
console.log(target.foo3); // undefined, assign method has finished, foo3 will not be copied.
console.log(target.baz); // undefined, the third source will not be copied either.
Copying accessors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const obj = {
foo: 1,
get bar() {
return 2;
}
};
let copy = Object.assign({}, obj);
console.log(copy);
// { foo: 1, bar: 2 }
// The value of copy.bar is obj.bar's getter's return value.
// This is an assign function that copies full descriptors
function completeAssign(target, ...sources) {
sources.forEach((source) => {
const descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
// By default, Object.assign copies enumerable Symbols, too
Object.getOwnPropertySymbols(source).forEach((sym) => {
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
}
copy = completeAssign({}, obj);
console.log(copy);
// { foo:1, get bar() { return 2 } }
Object.create()
In practice, objects with null
prototype are usually used as a cheap substitute for maps. The presence of Object.prototype
properties will cause some bugs:
1
2
3
4
5
6
7
8
9
10
11
12
const ages = { alice: 18, bob: 27 };
function hasPerson(name) {
return name in ages;
}
function getAge(name) {
return ages[name];
}
hasPerson("hasOwnProperty"); // true
getAge("toString"); // [Function: toString]
Using a null-prototype object removes this hazard without introducing too much complexity to the hasPerson
and getAge
functions:
1
2
3
4
5
6
7
const ages = Object.create(null, {
alice: { value: 18, enumerable: true },
bob: { value: 27, enumerable: true },
});
hasPerson("hasOwnProperty"); // false
getAge("toString"); // undefined
Making your object not inherit from Object.prototype
also prevents prototype pollution attacks. If a malicious script adds a property to Object.prototype
, it will be accessible on every object in your program, except objects that have null prototype.
1
2
3
4
5
6
7
8
9
const user = {};
// A malicious script:
Object.prototype.authenticated = true;
// Unexpectedly allowing unauthenticated user to pass through
if (user.authenticated) {
// access confidential data
}
Classical inheritance with Object.create()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info("Shape moved.");
};
// Rectangle - subclass
function Rectangle() {
Shape.call(this); // call super constructor.
}
// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype, {
// If you don't set Rectangle.prototype.constructor to Rectangle,
// it will take the prototype.constructor of Shape (parent).
// To avoid that, we set the prototype.constructor to Rectangle (child).
constructor: {
value: Rectangle,
enumerable: false,
writable: true,
configurable: true,
},
});
const rect = new Rectangle();
console.log("Is rect an instance of Rectangle?", rect instanceof Rectangle); // true
console.log("Is rect an instance of Shape?", rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
Note that there are caveats to watch out for using create()
, such as re-adding the constructor
property to ensure proper semantics. Although Object.create()
is believed to have better performance than mutating the prototype with Object.setPrototypeOf()
, the difference is in fact negligible if no instances have been created and property accesses haven’t been optimized yet. In modern code, the class syntax should be preferred in any case.
Using propertiesObject argument
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
let o;
// create an object with null as prototype
o = Object.create(null);
o = {};
// is equivalent to:
o = Object.create(Object.prototype);
// Example where we create an object with a couple of
// sample properties. (Note that the second parameter
// maps keys to *property descriptors*.)
o = Object.create(Object.prototype, {
// foo is a regular 'value property'
foo: {
writable: true,
configurable: true,
value: "hello",
},
// bar is a getter-and-setter (accessor) property
bar: {
configurable: false,
get() {
return 10;
},
set(value) {
console.log("Setting `o.bar` to", value);
},
},
});
function Constructor() {}
o = new Constructor();
// is equivalent to:
o = Object.create(Constructor.prototype);
// Of course, if there is actual initialization code
// in the Constructor function,
// the Object.create() cannot reflect it
// Create a new object whose prototype is a new, empty
// object and add a single property 'p', with value 42.
o = Object.create({}, { p: { value: 42 } });
// properties默认: NOT writable(不能删,值改不了), enumerable or configurable
// by default properties ARE NOT writable,
// enumerable or configurable:
o.p = 24;
o.p;
// 42
o.q = 12;
for (const prop in o) {
console.log(prop);
}
// 'q'
delete o.p;
// false
// to specify a property with the same attributes as in an initializer
o2 = Object.create(
{},
{
p: {
value: 42,
writable: true,
enumerable: true,
configurable: true,
},
},
);
// This is not equivalent to:
// o2 = Object.create({ p: 42 })
// which will create an object with prototype { p: 42 }
Symbol
Symbol
is a built-in object whose constructor returns a symbol
primitive — also called a Symbol value or just a Symbol — that’s guaranteed to be unique. Symbols are often used to add unique property keys to an object that won’t collide with keys any other code might add to the object, and which are hidden from any mechanisms other code will typically use to access the object.
Every Symbol()
call is guaranteed to return a unique Symbol. Every Symbol.for("key")
call will always return the same Symbol for a given value of "key"
. When Symbol.for("key")
is called, if a Symbol with the given key can be found in the global Symbol registry, that Symbol is returned. Otherwise, a new Symbol is created, added to the global Symbol registry under the given key, and returned.
1
Symbol('foo') === Symbol('foo') // false
1
const sym = new Symbol(); // TypeError
This prevents authors from creating an explicit Symbol
wrapper object instead of a new Symbol value and might be surprising as creating explicit wrapper objects around primitive data types is generally possible (for example, new Boolean
, new String
and new Number
).
If you really want to create a Symbol
wrapper object, you can use the Object()
function:
1
2
3
4
const sym = Symbol('foo');
typeof sym // "symbol"
const symObj = Object(sym);
typeof symObj // "object"
Shared symbols in the global Symbol registry
The above syntax using the Symbol()
function will create a Symbol whose value remains unique throughout the lifetime of the program. To create Symbols available across files and even across realms (each of which has its own global scope), use the methods Symbol.for()
and Symbol.keyFor()
to set and retrieve Symbols from the global Symbol registry.
1
Symbol.keyFor(Symbol.for("tokenString")) === "tokenString" // true
Well-known Symbols
All static properties of the Symbol
constructor are Symbols themselves, whose values are constant across realms. They are known as well-known Symbols, and their purpose is to serve as “protocols” for certain built-in JavaScript operations, allowing users to customize the language’s behavior. For example, if a constructor function has a method with Symbol.hasInstance
as its name, this method will encode its behavior with the instanceof
operator.
Finding Symbol properties on objects
The method Object.getOwnPropertySymbols()
returns an array of Symbols and lets you find Symbol properties on a given object. Note that every object is initialized with no own Symbol properties, so that this array will be empty unless you’ve set Symbol properties on the object.
Static properties
The static properties are all well-known Symbols. In these Symbols’ descriptions, we will use language like “Symbol.hasInstance
is a method determining…”, but bear in mind that this is referring to the semantic of an object’s method having this Symbol as the method name (because well-known Symbols act as “protocols”), not describing the value of the Symbol itself.
Symbol.asyncIterator
A method that returns the default AsyncIterator for an object. Used by for await...of
.
Symbol.hasInstance
A method determining if a constructor object recognizes an object as its instance. Used by instanceof
.
Symbol.isConcatSpreadable
A Boolean value indicating if an object should be flattened to its array elements. Used by Array.prototype.concat()
.
Symbol.iterator
A method returning the default iterator for an object. Used by for...of
.
Symbol.match
A method that matches against a string, also used to determine if an object may be used as a regular expression. Used by String.prototype.match()
.
Symbol.matchAll
A method that returns an iterator, that yields matches of the regular expression against a string. Used by String.prototype.matchAll()
.
Symbol.replace
A method that replaces matched substrings of a string. Used by String.prototype.replace()
.
Symbol.search
A method that returns the index within a string that matches the regular expression. Used by String.prototype.search()
.
Symbol.split
A method that splits a string at the indices that match a regular expression. Used by String.prototype.split()
.
Symbol.species
A constructor function that is used to create derived objects.
Symbol.toPrimitive
A method converting an object to a primitive value.
Symbol.toStringTag
A string value used for the default description of an object. Used by Object.prototype.toString()
.
Symbol.unscopables
An object value of whose own and inherited property names are excluded from the with
environment bindings of the associated object.
Static methods
Symbol.for(key)
Searches for existing Symbols with the given key
and returns it if found. Otherwise a new Symbol gets created in the global Symbol registry with key
.
Symbol.keyFor(sym)
Retrieves a shared Symbol key from the global Symbol registry for the given Symbol.
Instance methods
Symbol.prototype.toString()
Returns a string containing the description of the Symbol. Overrides the Object.prototype.toString()
method.
Symbol.prototype.valueOf()
Returns the Symbol. Overrides the Object.prototype.valueOf()
method.
Symbol.prototype[@@toPrimitive]()
Returns the Symbol.