I remember I saw a question on Zhihu before: should a person who is interviewing a 5-year front end, but can’t even figure out the prototype chain, and is full of Vue, React and other implementations be used? . When I was writing this article, I went back and looked at this question again. There were more than 300 answers, and many leaders answered this question, which shows that this issue is quite concerned. In recent years, with the popularity of ES6, TypeScript, and similar intermediate languages, we have had little access to prototypes in business development. We have mostly used ES6 classes to make it easier and more intuitive to do what constructors and prototypes used to do.

In fact, in my opinion, I think prototype chain is a very important basic knowledge. If a person says he is proficient in C, but he is not proficient in assembly, do you believe it? I think what Winter said is quite succinct and accurate:

This goes back to the interviewer’s skills that I talked about earlier, the interview is a systematic evaluation of a person’s ability, to figure out what a person is good at is not good, so ask informative questions, in order to avoid misjudgment, be sure to ask a lot of questions, systematic questions.

The prototype is not very revealing, at the very least it will have a huge disadvantage in terms of library design, and there may be a problem with learning habits, or it may be that they don’t know MUCH about JS at all, but that doesn’t mean that one question makes the person unusable.

This article includes the following:

  1. The prototype JavaScript
  2. Constructors andprototype
  3. Prototype chain
  4. Use of prototypes
  5. ES6 class and constructor relationship
  6. Prototype inheritance
  7. JavaScript and prototype-related syntax features
  8. The prototype of pollution
  9. Add an interview question related to the prototype that I encountered in the recent college recruitment interview

The prototype JavaScript

Any object has a prototype, and we can access the prototype of an object with the nonstandard __proto__ attribute:

// The stereotype of a pure object defaults to an empty object
console.log({}.__proto__); / / = > {}

function Student(name, grade) {
  this.name = name;
  this.grade = grade;
}

const stu = new Student('xiaoMing'.6);
// Prototype of the Student instance, also an empty object by default
console.log(stu.__proto__); // => Student {}
Copy the code

__proto__ is a nonstandard attribute. If you want to access an Object’s prototype, use the reflect.getPrototypeof or object.getPrototypeof () methods new in ES6. A nonstandard attribute means that the attribute may be modified or removed directly in the future. It may be removed if a new standard uses symbol.proto as the key to access the object’s prototype.

console.log({}.__proto__ === Object.getPrototypeOf({})); // => true
Copy the code

It is possible to modify an Object’s prototype by assigning the __proto__ attribute directly, preferably using ES6’s reflect.setPrototypeof or Object.setPrototypeof. Either way, the type of the value to be set can only be object or NULL. The other types have no effect:

const obj = { name: 'xiaoMing' };
// The prototype is an empty object
console.log(obj.__proto__); / / = > {}

obj.__proto__ = Awesome!;
// Non-objects and null do not take effect
console.log(obj.__proto__); / / = > {}

// Set the prototype as an object
obj.__proto__ = { a: 1 };
console.log(obj.__proto__); // => { a: 1 }
console.log(Reflect.getPrototypeOf(obj)); // => { a: 1 }
Copy the code

If the value set is not extensible, TypeError is raised:

const frozenObj = Object.freeze({});
// Object. IsExtensible (obj) determines whether obj isExtensible
console.log(Object.isExtensible(frozenObj)); // => false
frozenObj.__proto__ = null; // => TypeError: #<Object> is not extensible
Copy the code

If an object’s __proto__ is assigned to null, the situation is complicated, and you may find it strange to look at the following test:

const obj = { name: 'xiaoming' };

obj.__proto__ = null;
/ /! : why not null, as if __proto__ was deleted
console.log(obj.__proto__); // => undefined
// The prototype is indeed set to null
console.log(Reflect.getPrototypeOf(obj)); // => null

// Assign null again
obj.__proto__ = null;
// Black question mark?? Why is not the previous undefined?
console.log(obj.__proto__); // => null

obj.__proto__ = { a: 1 };
console.log(obj.__proto__); // => { a: 1 }
// __proto__ as a normal attribute obj. XXX = {a: 1}
// The prototype was not set up successfully
console.log(Reflect.getPrototypeOf(obj)); // => null

Reflect.setPrototypeOf(obj, { b: 2 });
// When __proto__ is set to null, obj's __proto__ attribute is no different from a normal attribute
console.log(obj.__proto__); // => { a: 1 }
// Use reflect.setPrototypeof to set the prototype
console.log(Reflect.getPrototypeOf(obj)); // => { b: 2 }
Copy the code

__proto__ is an accessor property defined on object. prototype. The getter and setter define the accessor property for __proto__. That’s the prototype. Here is the code for the __proto__ behavior THAT I simulated, and note that the following code is set to null:

const weakMap = new WeakMap(a);Object.prototype = {
  get __proto__() {
    return this['[[prototype]]'= = =null ? weakMap.get(this) : this['[[prototype]]'];
  },
  set __proto__(newPrototype) {
    if (!Object.isExtensible(newPrototype)) throw new TypeError(`${newPrototype} is not extensible`);

    const isObject = typeof newPrototype === 'object' || typeof newPrototype === 'function';
    if (newPrototype === null || isObject) {
      // If previously set to null by __proto__
      // It is useless to modify the prototype by assigning __proto__
      Obj.__proto__ = {a: 1} just like a normal property obj.xxx = {a: 1}
      if (this['[[prototype]]'= = =null) {
        weakMap.set(this, newPrototype);
      } else {
        this['[[prototype]]'] = newPrototype; }}},/ /... Other properties such as toString, hasOwnProperty, etc
};
Copy the code

In summary: If an object’s __proto__ attribute is assigned to null, then its prototype has already been changed to NULL. However, if you want to set the prototype by assigning __proto__ to null, then __proto__ is no different from a normal attribute. You can only modify a prototype with reflect. setPrototypeOf or Object.setPrototypeOf. SetPrototypeOf can modify the prototype because it directly modifies the prototype property of the object, i.e. internally assigns the [[prototype]] property of the object. Instead of going through the __proto__ getter.

Constructor and Prototype

In JavaScript, functions can be used as constructors. A constructor can be called a class, but a Student constructor can be called a Student class. We can construct an instance using the new constructor. It is customary to use big camel name for functions used as constructors:

function Apple() {}
const apple = new Apple();
console.log(apple instanceof Apple); // => true
Copy the code

Any constructor has a prototype property, which defaults to an empty pure object to which all instances constructed by the constructor refer.

// The prototype of the instance is apple1.__proto__
console.log(apple1.__proto__ === Apple.prototype); // => true
console.log(apple2.__proto__ === Apple.prototype); // => true
Copy the code

The prototype property of the constructor defaults to an empty object. Note that the object has no traversable properties:

console.log(Apple.prototype); // => Apple {}
console.log(Object.keys(Apple.prototype)); / / = > []
console.log(Apple.prototype.__proto__ === {}.__proto__); // true
Copy the code

The constructor’s prototype has a constructor property that points to the constructor itself:

console.log(Apple.prototype.constructor === Apple); // => true
Copy the code

The constructor property is non-iterable and can be interpreted as internally defining the property as follows:

Object.defineProperty(Apple.prototype, 'constructor', {
  value: Student,
  writable: true.// Not enumerable, not available through object.keys ()
  enumerable: fasle,
});
Copy the code

__proto__, prototype, constructor, Apple constructor

Some people might confuse __proto__ with prototype. In translation terms, they can both be called prototypes, but they are two very different things.

Function prototype is the __proto__ of all instances constructed using the new function. Functions are also objects, so functions have both __proto__ and prototype.

Note: If I refer to a constructor prototype in this article, I am referring to the constructor’s __proto__, not the constructor’s Prototype property.

Prototype chain

So what are the characteristics of an object’s prototype?

When accessing a property on an object obj that does not exist in obj, the object’s prototype is obj.__proto__ to look for the property. If so, return this property. If not, go to the prototype of the object obj, which is obj.__proto__.__proto__. Object.prototype.__proto__ (null) returns undefined.

Here’s an example:

function Student(name, grade) {
  this.name = name;
  this.grade = grade;
}

const stu = new Student();
console.log(stu.notExists); // => undefined
Copy the code

The whole process of visiting stu.notExists is:

  1. See firststuExists onnotExistsIt doesn’t exist, so lookstu.__proto__
  2. stu.__proto__Neither does thenotExistsProperties, let’s seestu.__proto__.__proto__In fact, it isPure objectThe prototype:Object.prototype
  3. Pure objectIt doesn’t exist on the prototypenotExistsProperties, and then you go up tostu.__proto__.__proto__.__proto__If you go up there, it’s null
  4. Null was not foundnotExistsProperty, returns undefined

Some of you might look at this and wonder, “Look for the prototype of an object and you’ll find the prototype of a pure object.” Right? Take the test and find out:

console.log(stu.__proto__.__proto__ === {}.__proto__); // => true
Copy the code

The prototype of a pure object’s prototype is NULL:

console.log(new Object().__proto__.__proto__); // => null
Copy the code

The chain formed between each prototype is called the prototype chain.

Think about what the prototype chain of the Student function should look like.

Use of prototypes

When defining a type using a constructor, we usually define the class’s methods on the stereotype, which is a perfect match for the pointing property of this.

function Engineer(workingYears) {
  this.workingYears = workingYears;
}

// Arrow functions cannot be used. The arrow function's this is declared in context
Engineer.prototype.built = function () {
  // this is the function caller
  console.log('I've been workingThe ${this.workingYears}For years, my job is to turn the screws... `);
};

const engineer = new Engineer(5);
// This points correctly to the instance, so this.workingYears is 5
engineer.built(); // => I have been working for 5 years, my job is to turn screws...
console.log(Object.keys(engineer)); // => [ 'workingYears' ]
Copy the code

This way, all instances can access the method, and the method takes up only one memory, saving memory, and pointing to this correctly points to the instance of the class.

Keys () is not a property of its own:

const obj = {
  func() {},
};

console.log(Object.keys(obj)); // => [ 'func' ]

function Func() {}
Func.prototype.func = function () {};
console.log(Object.keys(new Func())); / / = > []
Copy the code

If you want to define instance attributes, you’ll have to use this. XXX = XXX to define instance methods:

function Engineer(workingYears) {
  this.workingYears = workingYears;
  this.built = function () {
    console.log('I've been workingThe ${this.workingYears}For years, my job is to turn the screws... `);
  };
}

const engineer = new Engineer(5);
console.log(Object.keys(engineer)); // => [ 'workingYears', 'built' ]
Copy the code

Actually many methods in JavaScript are defined in the constructor’s prototype, such as Array. Prototype. Slice, Object. The prototype. ToString, etc.

ES6 class and constructor relationship

Many languages have object-oriented programming paradigms, such as Java, c#, python, etc. ES6 classes make object-oriented programming easier for developers who switch from them to JavaScript.

ES6 class

In essence, ES6 class is the syntactic sugar of the constructor. Let’s take a look at what Babel compiled into ES6 class:

The original code:

class Circle {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
  }

  draw() {
    console.log('Let me draw the coordinates of (The ${this.x}.The ${this.y}), the radius isThe ${this.r}The circular `); }}Copy the code

Result of Babel + babel-preset-es2015-loose

'use strict';

// Circle class can be understood as a constructor function
var Circle = (function () {
  function Circle(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
  }

  var _proto = Circle.prototype;

  The class method is defined on Prototype
  _proto.draw = function draw() {
    console.log(
      '\u753B\u4E2A\u5750\u6807\u4E3A (' +
        this.x +
        ', ' +
        this.y +
        ')\uFF0C\u534A\u5F84\u4E3A ' +
        this.r +
        ' \u7684\u5706'
    );
  };

  returnCircle; }) ();Copy the code

ES6’s class is the constructor, and the methods on the class are defined in the constructor’s Prototype.

Extends inheritance

Let’s look again at how the transformation is done using the extends inheritance.

The original code:

class Shape {
  constructor(x, y) {
    this.x = x;
    this.y = y; }}class Circle extends Shape {
  constructor(x, y, r) {
    super(x, y);
    this.r = r;
  }

  draw() {
    console.log('Let me draw the coordinates of (The ${this.x}.The ${this.y}), the radius isThe ${this.r}The circular `); }}Copy the code

Result of Babel + babel-preset-es2015-loose

'use strict';

// Prototype inheritance
function _inheritsLoose(subClass, superClass) {
  subClass.prototype = Object.create(superClass.prototype);
  subClass.prototype.constructor = subClass;
  // Give subclasses access to static properties on the parent class, which are defined on the constructor itself
  // For example, if the parent class has the person. say attribute, the child class Student can be accessed through Student
  subClass.__proto__ = superClass;
}

var Shape = function Shape(x, y) {
  this.x = x;
  this.y = y;
};

var Circle = (function (_Shape) {
  _inheritsLoose(Circle, _Shape);

  function Circle(x, y, r) {
    var _this;

    // Combinatorial inheritance
    _this = _Shape.call(this, x, y) || this;
    _this.r = r;
    return _this;
  }

  var _proto = Circle.prototype;

  _proto.draw = function draw() {
    console.log(
      '\u753B\u4E2A\u5750\u6807\u4E3A (' +
        this.x +
        ', ' +
        this.y +
        ')\uFF0C\u534A\u5F84\u4E3A ' +
        this.r +
        ' \u7684\u5706'
    );
  };

  return Circle;
})(Shape);
Copy the code

The entire EXTENDS of ES6 implements archetypal inheritance + composite inheritance.

The subclass constructor calls the superclass constructor and points this to the subclass instance to combine the instance attributes of the superclass to the subclass instance:

// Combinatorial inheritance
_this = _Shape.call(this, x, y) || this;
Copy the code

The _inheritsLoose function implements the stereotype inheritance described in the next section.

Prototype inheritance

Let’s talk about inheritance before we talk about archetypal inheritance. I think the common sense of inheritance is that if class A inherits from class B, then instances of A inherit from instance attributes of B.

Archetypal inheritance requires that an instance of A inherit properties from the archetypal properties of B.

Define archetypal inheritance:

For classes A and B, if a.prototype.__proto__ === B.prototype is satisfied, then A prototype inherits BCopy the code

In fact, the above definition is too strict, I think as long as B’s prototype is on the chain of A’s prototype, so that you can already access the attributes of B’s prototype on the instance of A, the above definition can be said to be direct inheritance, but can be two or more levels of inheritance.

How do you implement prototype inheritance? A. protoType === new B();

function A() {}
function B() {
  this.xxx = 'The prototype of Pollution A';
}

A.prototype = new B();

console.log(A.prototype.__proto__ === B.prototype); // => true
Copy the code

But this approach causes instance attributes of B to contaminate A’s prototype. The solution is to bridge the prototype chain with an empty function that has no instance attributes:

function A(p) {
  this.p = p;
}

function B() {
  this.xxx = 'Contamination prototype';
}

/ / empty function
function Empty() {}

Empty.prototype = B.prototype;
A.prototype = new Empty();
// Correct the constructor direction
A.prototype.constructor = A;

// Meet the definition of stereotype inheritance
console.log(A.prototype.__proto__ === B.prototype); // => true

const a = new A('p');
console.log(a instanceof A); // => true

const b = new B();
console.log(b instanceof B); // => true

// A is also an instance of B
console.log(a instanceof B); // => true
console.log(a.__proto__.__proto__ === B.prototype); // => true
Copy the code

Use Windows built-in graphics software to draw the prototype chain _〆(´ д ‘) :

With Object.create, we can implement prototype inheritance more easily, which is what Babel uses above: _inheritsLoose

function _inheritsLoose(subClass, superClass) {
  // object.create (prototype) Returns an Object based on prototype
  subClass.prototype = Object.create(superClass.prototype);
  subClass.prototype.constructor = subClass;
  The prototype inheritance we implemented above does not set this, but the class inheritance sets the prototype of the subclass to the parent class
  subClass.__proto__ = superClass;
}
Copy the code

JavaScript and prototype-related syntax features

In fact, many syntax features are related to prototypes. When talking about prototypes, we will continue to talk about some knowledge points related to prototypes in JavaScrip syntax features.

New operator principle

What happens when we use new on the function.

To describe it in code:

function isObject(value) {
  const type = typeof value;
  returnvalue ! = =null && (type === 'object' || type === 'function');
}

/** * constructor indicates the new constructor * args indicates the argument passed to the constructor */
function New(constructor, ... args) {
  // If the new object is not a function, TypeError is raised
  if (typeof constructor! == 'function') throw new TypeError(`${constructor} is not a constructor`); Const target = object.create () const target = object.create ();constructor.prototype); // Point the constructor's this to the empty object created in the previous step and execute, in order to add the instance attribute const result = to thisconstructor.apply(target, args); Target return isObject(result)? result : target; }Copy the code

Here’s a quick test:

function Computer(brand) {
  this.brand = brand;
}

const c = New(Computer, 'Apple');
console.log(c); // => Computer { brand: 'Apple' }
Copy the code

The instanceof operator principle

Instanceof is used to determine if an object is an instanceof A class. If obj instance A, we say obj is an instanceof A.

The principle is simple, in A word: obj instanceof constructor A is equivalent to determining whether A’s prototype is A prototype of OBJ (or possibly A second-level prototype).

Code implementation:

function instanceOf(obj, constructor) {
  if(! isObject(constructor)) {
    throw new TypeError(`Right-hand side of 'instanceof' is not an object`);
  } else if (typeof constructor! == 'function') {throw new TypeError(`Right-hand side of 'instanceof' is not callable`);
  }

  // This is the main sentence
  return constructor.prototype.isPrototypeOf(obj);
}
Copy the code

Here’s a quick test:

function A() {}
const a = new A();

console.log(a instanceof A); // => true
console.log(instanceOf(a, A)); // => true
Copy the code

The prototype of pollution

In the fall of 2019, when I was working as an intern in a large factory in China, LoDash exposed a serious security vulnerability: The LoDash library exposed a serious security vulnerability, affecting more than 4 million projects. The security breach was caused by prototype contamination.

Prototype contamination refers to:

The attacker somehow modifies the prototype of a JavaScript object

If any one of the prototypes is contaminated, it could cause problems.

Hazards of prototype contamination

Performance issues

Here’s the simplest example:

Object.prototype.hack = 'Properties of contaminated archetypes';
const obj = { name: 'xiaoHong'.age: 18 };
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(obj[key]); }}/* =>
xiaoHong
18
*/
Copy the code

Contamination of the stereotype increases the number of traversals, and every time you access a property that doesn’t exist on the object itself, you also need to access the tainted property on the stereotype.

Logic bug that causes an accident

Take a look at a specific node security vulnerability case:

'use strict';

const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');

const isObject = (obj) = > obj && obj.constructor && obj.constructor === Object;

function merge(a, b) {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else{ a[attr] = b[attr]; }}return a;
}

function clone(a) {
  return merge({}, a);
}

// Constants
const PORT = 8080;
const HOST = '127.0.0.1';
const admin = {};

// App
const app = express();
app.use(bodyParser.json());
app.use(cookieParser());

app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
  var body = JSON.parse(JSON.stringify(req.body));
  var copybody = clone(body);
  if (copybody.name) {
    res.cookie('name', copybody.name).json({
      done: 'cookie set'}); }else {
    res.json({
      error: 'cookie not set'}); }}); app.get('/getFlag', (req, res) => {
  varа dmin =JSON.parse(JSON.stringify(req.cookies));
  if(admin. а dmin = =1) {
    res.send('hackim19{}');
  } else {
    res.send('You are not authorized'); }}); app.listen(PORT, HOST);console.log(`Running on http://${HOST}:${PORT}`);
Copy the code

This code is vulnerable to the merge function, which we can attack as follows:

curl -vv --header 'Content-type: application/json' -d '{"__proto__": {"admin": 1}}' 'http://127.0.0.1:4000/signup';

curl -vv 'http://127.0.0.1/getFlag'
Copy the code

We first call the /signup interface. In the NodeJS service, we call the merge method, which is vulnerable, Prototype (because {}.__proto__ === Object.prototype) adds a new attribute admin with a value of 1.

Admin.а dmin == 1 = true, the service is attacked.

Prevention of prototype contamination

Most prototype contamination occurs when calling functions that modify or extend object properties, such as LoDash’s defaults and jquery’s extend. The most important thing to prevent prototype pollution is to have a sense of prevention and develop good coding habits.

Object.create(null)

I often see this operation when I look at the source code for some libraries, such as EventEmitter3. Create (null) creates an Object with no prototype, even if you set __proto__ to it, because its prototype is null in the first place and there is no setter for __proro__.

const obj = Object.create(null);
obj.__proto__ = { hack: 'Properties of contaminated archetypes' };
const obj1 = {};
console.log(obj1.__proto__); / / = > {}
Copy the code

Object.freeze(obj)

You can use object.freeze (obj) to freeze an Object obj. The frozen Object cannot be modified and becomes an unextensible Object. We can’t modify the prototype of an unextensible object. TypeError is thrown:

const obj = Object.freeze({ name: 'xiaoHong' });
obj.xxx = Awesome!;
console.log(obj); // => { name: 'xiaoHong' }
console.log(Object.isExtensible(obj)); // => false
obj.__proto__ = null; // => TypeError: #<Object> is not extensible
Copy the code

It’s been nearly three months since I left the company where I interned, and I remember that every time NPM install checked for dozens of dependency vulnerabilities. It must be because I haven’t upgraded for a long time that I accumulated so many bugs. Anyway, I dare not upgrade at random. A bug I checked for a long time before was caused by axios upgrade. Also do not know up to now has upgraded 😄.

An interview question related to the prototype encountered in a recent college recruitment interview

I was recently interviewed by a large company.

function Page() {
  return this.hosts;
}
Page.hosts = ['h1'];
Page.prototype.hosts = ['h2'];

const p1 = new Page();
const p2 = Page();

console.log(p1.hosts);
console.log(p2.hosts);
Copy the code

TypeError: Cannot read property ‘hosts’ of undefined is displayed.

If you return the object with new, the object is directly used as the result of new, so P1 should be the result of this.hosts. In new Page(), this is a page.prototype target object, so this.hosts can access page.prototype. hosts ([‘h2’]). So p1 is equal to [‘h2’], [‘h2’] has no hosts property so return undefined.

Console. log(p2.hosts) returns an error. P2 is the result of a call to the Page constructor directly. This refers to the global object, which has no hosts property. Accessing hosts from undefined is an error.

References:

  1. Latest: what you Have to know about JavaScript behind Lodash’s serious security Flaw

This article is original content, first published in personal blog, reproduced please indicate the source.