Dive deep into JavaScript prototype and closure

Dive deep into JavaScript prototype and closure

This article is essentially a translated version of 《深入理解javascript原型和闭包》by 王福朋

1. Everything is Object

Except following value types, all other types in JS is object type.

console.log(typeof x);    // undefined
console.log(typeof 10);   // number
console.log(typeof 'abc'); // string
console.log(typeof true);  // boolean

e.x.

console.log(typeof function () {});  //function

console.log(typeof [1, 'a', true]);  //object
console.log(typeof { a: 10, b: 20 });  //object
console.log(typeof null);  //object
console.log(typeof new Number(10));  //object

var fn = function () { };
console.log(fn instanceof Object);  // true

Definition: Object is a collection of several fields(attributes).

let obj = {
	a: 10,
  b: function (x) {
    console.log(x);
  },
  c: {
    name: 'Joey',
    age: 24
  }
};

In the example above, a, b, c are three fields. refer to a value type, a function(object), a object with two fields.

2. Relationship between function and object

Definition: function is a special type of object, and all objects are created by function.

Explicit:

function fn() {
  this.name = 'Joey';
  this.age = 24;
}
let obj1 = new fn();

Implicit:

let a = { name: 'Joey', age: 24};
let b = [5, 'x', true];

is basically:

let a = new Object();
obj.name = 'Joey';
obj.age = 24;
let b = new Array();
b[0] = 5;
b[1] = 'x';
b[2] = true;

However, the Object and Array in the above code are all functions.

console.log(typeof (Object));  // function
console.log(typeof (Array));  // function

3. Prototype

Definition: every function has a default field: prototype, prototype is a Object. with a field called constructor by default, pointing to the function itself.

The prototype in Object class has several other fields, like shown below (hasOwnProperty, isPrototypeOf…etc).

More fields can also be added into the protytype of a function.

function fn() {
	fn.prototype.name = 'Joey';
  fn.prototype.getAge = () => {
    return 24;
  }
}

Attributes in the prototype of a function can be referenced by Object created with this function.


function Fn() { }
  Fn.prototype.name = 'Joey';
  Fn.prototype.getYear = function () {
  return 1988;
};

var fn = new Fn();
console.log(fn.name);
console.log(fn.getYear());

Every object has a hiden field __proto__, which holds a reference to the prototype field of the creation function of this object. This __proto__ is called a implicit prototype.

fn.__proto__ === Fn.prototype

4. Implicit prototype

let obj = {};
console.log(obj.__proto__);

The output of the above code is the same as Object.prototype

Every object has a __proto__ field, which refer to the prototype of the creation function.

The prototype of self defined function is essentially an object, with fn.prototype.__proto__ field refer to Object.prototype.

Object.prototype is a object itself, but a special case happens here: Object.prototype.__proto__ is null.

Two types of creating function:

function add(x, y) {
	return x + y;
}
let add1 = new Function('x', 'y', 'return x + y;');

All functions are created by Function, which means add.__proto__ === Function.prototype

5. instanceof: proto-chain

When applying typeof primitive to reference types, return values consist of object/function only. If want to know the exact type of an instance, instanceof should be applied.

The instanceof operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object. The return value is a boolean value.

console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function instanceof Function); // true

Object.__proto__ === Function.prototype

Function.__proto__.__proto__ === Object.prototype

Function.__proto__ === Function.prototype

6. inheritance

The inheritance in JS is represented by proto-chain.

function Foo() {}
let f1 = new Foo();
f1.a = 10; // basic field
Foo.prototype.a = 100; // proto-chain
Foo.prototype.b = 200; // proto-chain
console.log(f1.a); // 10
console.log(f1.b); // 200

Proto-chain: when accessing the field of an object, first retrieve in the basic fields in the object. If cannot find in basic fields, the field should be retrived alongside the proto-chain.

How to identify whether an field to be basic field or found in the proto-chain? We should use hasOwnProperty method.

let fn = function(){}
fn.prototype.a = 3;
let f1 = new fn();
console.log(f1.hasOwnProperty('a')); // false
f1.a = 10;
console.log(f1.hasOwnProperty('a')); // true

Code below shows how to find basic fields of an object.

for (item in f1) {
  if (f1.hasOwnProperty(item)) {
    console.log(item);
  }
} 

7. Flexibility of prototype

The prototype in JS is very flexible:

  1. The object fields can be added or modofied at any time.

In JQuery, the object holds barely anything while created, but added with code processing.

  1. The inherited items can be modified(override) if not applicable or inappropriate.
let obj = { a: 10, b: 20};
console.log(obj.toString()); // [object Object]
let arr = [1, 2, true];
console.log(arr.toString()); // 1, 2, true

function Foo() {}
let f1 = new Foo();
Foo.prototype.toString = function() {
  return 'Joey';
};
console.log(f1.toString()); // Joey

Additional methods can be added, e.x.:

Judge the existence of method before adding prototype method to a internal function, and add if not exist.

8 & 9. Execution context

Execution context: Before execution of the code, prepare the variables to be used with undefined or assign with value.

  1. Preparation state with variable declaration (without value assignment)

console.log(a); // Uncaught ReferenceError: a is not defined
console.log(a); // undefined
var a;
console.log(a); // undefined
var a = 10;
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 10;

Tips to understand above behavior (variable hoisting):

  1. All declarations (function, var, let, const and class) are hoisted in JavaScript, while the var declarations are initialized with undefined, but let and const declarations remain uninitialized.

  2. Let declared variables will only get initialized when their lexical binding (assignment) is evaluated during runtime by the JavaScript engine. This means you can’t access the variable before the engine evaluates its value at the place it was declared in the source code. This is what we call “Temporal Dead Zone”, A time span between variable creation and its initialization where they can’t be accessed.

  1. Preparation state with variable declaration and value assignment

  1. Preparation state with variable declaration and value assignment
  1. function declaration
console.log(f1); // ƒ f1(){ return a;}
function f1(){ return a;}
  1. funciton expression
console.log(f1); // undefined
var f1 = function (){ return a;}

function declaration assigned the value in preparation state, however, function expression is the same as the var declared variables.

In the preparation state, JS engine finished:

  1. var declared varibles and function expressions declaration, default value as undefined

  2. value assignment to this
  3. value assignment to function declaration

Every time a function is invoked, a new execution context is created.

e.x. Code belows shows in this invocation, the arguments is assigned with [10], and x is assigned with 10.

10. Execution context stack

When executing global code, it generates a execution context. Every time, an invocation to a function also creates a execution context, and when the function completes execution , this execution context and all data will be removed.The engine always have one and only one activating execution context.

var a = 10, fn, bar = function (x) {
	var b = 5;
  fn(x + b);
}

fn = function (y) {
  var c = 5;
  console.log(y + c);
}

bar(10);

Global execution context:

a 10
fn function
bar function
this window

bar- execution context:

b undefined
x 10
arguments [10]
this window

fn- execution context

c undefined
y 15
arguments [15]
this window

11. this

The value of this is determined by when the function is called, but not declared. The value of this is part of the execution context.

Here are several cases:

  1. constructor function

Constructor is function to new an object. Strictly speaking, every function can be used to new objects. However, some function are born to initialize objects, while others not. By code style, the fucntion name of a constructor function should in capital. e.x. Object, Array, Function.

 // Foo is used as constructor
 function Foo() {
 	this.name = 'Joey';
 	this.age = 24;
 	console.log(this); // Foo {name: 'Joey', age: 24}
 }
 
 let f1 = new Foo();
 
 console.log(f1.name); // Joey
 console.log(f1.age); // 24
// Foo is used as normal function
function Foo() {
	this.name = 'Joey';
	this.age = 24;
	console.log(this); // Window {0: Window, window: Window, self: Window, document: document, name: 'Joey', location: Location, …}
}
Foo();
  1. function is used as a field of object
// 2.1 function is called as a field of an object
let obj = {
  x: 10,
  fn: function() {
    console.log(this);   // {x: 10, fn: ƒ}
    console.log(this.x); // 10
  }
};
obj.fn();
// 2.2 function is called by another reference variable
let obj = {
  x: 10,
  fn: function() {
    console.log(this);   // Window {0: Window, window: Window, self: Window, document: document, name: 'Joey', location: Location, …}
    console.log(this.x); // undefined
  }
};
let fn1 = obj.fn;
fn1();
  1. function is called via call or apply
let obj = {
  x: 10
};
let fn = function() {
    console.log(this);   // {x: 10}
    console.log(this.x); // 10
  };
fn.call(obj);
  1. global & invoke normal functions
// under global setting, `this` is equal to window
console.log(this === window); // true
// when normal function is invoked, `this` is equal to window
var x = 10;
let fn = function() {
  console.log(this); // Window {0: Window, window: Window, self: Window, document: document, name: 'Joey', location: Location, …}
  console.log(this.x); // 10
};
fn();
// function declared inside another function is still a normal function , `this` is equal to window
let obj = {
  x: 10,
  fn: function() {
		function f() {
      console.log(this);   // Window {0: Window, window: Window, self: Window, document: document, name: 'Joey', location: Location, …}
      console.log(this.x); // undefined
    }
    f();
  }
};
obj.fn();
  1. Reuse of this in consecutive assignment

    jQuery.extend = jQuery.fn.extend = function() {
    	...
    	target = this;
    	// `this` refers to jQuery when executing jQuery.extend
    	// `this` refers to jQuery.fn when executing jQuery.fn.extend
    	...
    }
    
  2. this in the prototype or proto-chain, this refers to the invoked object

function Fn() {
	this.name = 'Joey';
  this.age = 24;
}

Fn.prototype.getName = function() {
  console.log(this.name); // `this` refers to the invoked object
}

let f1 = new Fn();
f1.getName(); // Joey
f1.name = 'Harris';
f1.getName(); // Harris

12. Scope

Every function has its own scope, which is identified when the function is created.

var a = 10, b = 20; // global scope
function fn() {
  var a = 100, c = 300; // fn scope
  console.log(a); // 100
  function bar() {
    var a = 1000, d = 4000; // bar scope
  }
}
var a = 10, b = 20; // global scope
function fn() {
  var c = 300; // fn scope
  console.log(a); // 10
  function bar() {
    var a = 1000, d = 4000; // bar scope
  }
}

Scope can be used to separate variables, under different scope, the variable declaration will not have confliction.

Scope will have subordinate relationships, which are identified by the scope creation. e.x.: bar function is declared within fn function, then fn scope is the upper level of bar scope, the variables declared in fn scope can be used in bar scope.

The outermost level of jQuery is an automatically executed anonymous function.

In this way, the varibles declared in jQuery can be isolated in a separate scope, without interfering with outer JavaScript code variables.

13. Scope and Context

var a = 10, b = 20; // global scope
function fn(x) {
	var a = 100, c = 300; // fn scope 
	function bar(x) {
		var a = 1000, d = 4000;	// bar scope
	}
  bar(100);
  bar(200);
}
fn(10);

14. From [free variable] to [scope-chain]

Definition: A free variable is a variable used but not declared in a scope. e.x. x is a free variable regarding to fn scope in the code below.

var x = 10;

function fn() {
  var b = 20;
  console.log(x + b); // x is a free variable here
}

The value of a free variable should be read from the scope that created the current scope, or any upper-level scope along the scope-chain.

var x = 10;

function fn() {
  console.log(x);
}

function show(f) {
  var x = 20;
  (function() {
    f(); // 10
  })();
}

show(fn);

An exmple of scope-chain:

15. Closure

Closure corresponds to two situations:

  1. function as a return value
  2. function as a passing argument
  1. function as a return value
function fn() {
  var max = 10;
  
  return function bar(x) {
    if (x > max) {
    	console.log(max + " " + x);  // 10 15  
    }
  }
}

var f1 = fn();
f1(15);
  1. function as a passing argument
var max = 10, fn = function(x) {
	if (x > max) {
    console.log(max + " " + x);  // 10 15  
  }
};
(function(f) {
	var max = 100;
	f(15);
})(fn);

In some cases, when a function call completes, its execution context is not destroyed, this is caused by closure.

function fn(x) {
  var max = 10;
  return function bar(x) {
    if (x > max) {
      console.log(max + " " + x);  // 10 15  
    }
  }
};

var f1 = fn(), max = 100;
f1(15);
function fn(x) {
//   var max = 10;
  return function bar(x) {
    console.log(max + " " + x); // 100 15
  }
};

var f1 = fn(), max = 100;
f1(15);

fn() execution context will be in active status, because the closure needs variables in the execution context.

In the example below, when f2() is invoked, the execution context of fn(5) still exists in memory. This is an example of Multiple execution contexts exist in a single scope.

Reference:

[1]. https://www.cnblogs.com/wangfupeng1988/p/3977924.html

[2]. https://blog.bitsrc.io/hoisting-in-modern-javascript-let-const-and-var-b290405adfda