一名【合格】前端工程师的自检清单(原型和原型链篇)

 

##原型和原型链

 

###1. 理解原型设计模式以及JavaScript中的原型规则

 

####原型设计模式

 

JavaScript是一种基于对象的语言, JavaScript中的所有对象, 都具有prototype属性。 prototype属性返回对象的所有属性和方法, 所有 JavaScript 内部对象都有只读的 prototype 属性, 可以向其原型中动态添加属性和方法, 但该对象不能被赋予不同的原型。 但是自定义的对象可以被赋给新的原型。

 

对象分为函数对象和普通对象, 区分: 凡是通过 new Function() 创建的对象都是函数对象, 其他的都是普通对象。 (Object , Function 是JS自带的函数对象)

 

1. 原型对象: prototype

 

在JS 中, 函数对象其中一个属性: 原型对象 prototype。 普通对象是没有prototype属性, 但有__proto__属性。

 

原型的作用就是给这个类的每一个对象都添加一个统一的方法, 在原型中定义的方法和属性都是被所以实例对象所共享。

 

  例:

 

     var person = function(name){

       this.name = name

     };

     person.prototype.getName = function(){//通过person.prototype设置函数对象属性

 

return this.name;

 

     }

     var zxj= new person(‘zhangxiaojie’);

    zxj.getName(); //zhangxiaojie     //zxj继承上属性

 

2. 原型链

 

__proto__: js创建对象的内置属性, 用于指向创建它的函数对象的原型对象prototype。 (是JS内部使用寻找原型链的属性。 当实例化一个对象时候, 内部会新建一个__proto__属性并把prototype赋值给它)

 

下图为原型链运行图解, 当我们实例一个对象之后, 调用它的内部方法, 他的运行顺序是先找自身有没有该方法, 如果没有就会通过__proto__属性想上层寻找, 一直寻找到Object对象中, 如果还没有才会报错null

 

p._proto_----->Persion._proto_---->object._proto_----->null


 

3. 设计模式

 

(1)工厂模式: 在函数内创建一个对象, 给对象赋予属性及方法再将对象返回。

 

   function Parent(){

 

var Child = new Object();

Child.name = "ZXJ";

Child.age = "24";

 

     Child.sex=function(){

 

return "女";

 

     };

 

return Child;

};

 

调用:

var x = Parent();

 

   alert(x.name); //ZXJ

   alert(x.sex()); //女

 

(2)构造函数模式:无需再函数内部重建创建对象, 而使用this指代

 

     function Parent(){

 

var Child = new Object();

this.name = "ZXJ";

this.age = 24 ";

this.lev = lev;

 

      Child.sex=function(){

 

return "女";

 

     };

 

return Child;

};

 

调用:

var x = new Parent();

 

   alert(x.name); //ZXJ

   alert(x.sex()); //女

 

  (3)原型模式: 函数中不对属性进行定义, 利用prototype属性对属性进行定义, 可以让所有对象实例共享它所包含的属性及方法。

 

    function Parent(){ };

 

Parent.prototype.name = "ZXJ";

Parent.prototype.age = "24";

Parent.prototype.sex = function() {

 

        var s="女";

 

        alert(s);

 

      };

 

     调用:

 

var x = new Parent();

alert(x.name); //ZXJ

 

   alert(x.sex()); //女


 

  (4)混合模式: 原型模式+构造函数模式。 这种模式中, 构造函数模式用于定义实例属性, 而原型模式用于定义方法和共享属性。

 

     function Parent(){

 

this.name = "ZXJ";

this.age = 24;

};

 

   Parent.prototype.sayname=function(){

 

return this.name;

 

   };

 

调用:

 

   var x =new Parent();

   alert(x.sayname()); //ZXJ  


 

  (5)动态原型模式:将所有信息封装在了构造函数中, 而通过构造函数中初始化原型, 这个可以通过判断该方法是否有效而选择是否需要初始化原型。


 

    function Parent(){

 

this.name = "ZXJ";

this.age = 24;

if (typeof Parent._sayname == "undefined") {

Parent.prototype.sayname = function() {

return this.name;

}

Parent._sayname = true;

}

 

};

调用:

var x = new Parent();

alert(x.sayname());

 

####原型规则

 

A. 所有的引用类型(数组、 对象、 函数), 都具有对象特性, 即可自由扩展属性;

  var arr = [];

  arr.a = 1;

B. 所有的引用类型(数组、 对象、 函数), 都有一个_proto_属性(隐式原型), 属性值是一个普通的对象;

C. 所有的函数, 都具有一个prototype(显式原型), 属性值也是一个普通对象;

D. 所有的引用类型(数组、 对象、 函数), 其隐式原型指向其构造函数的显式原型; (obj._proto_ === Object.prototype);

E. 当试图得到一个对象的某个属性时, 如果这个对象本身没有这个属性, 那么会去它的_proto_(即它的构造函数的prototype)中去寻找;

 

###2.instanceof的底层实现原理, 手动实现一个instanceof

 

####instanceof底层是如何工作的:

 

function instance_of(L, R) {//L 表示左表达式, R 表示右表达式

 

var O = R.prototype; // 取 R 的显示原型

 

L = L.__proto__; // 取 L 的隐式原型

 

while (true) {

 

if (L === null)

 

return false;

 

if (O === L) // 当 O 显式原型 严格等于 L隐式原型 时,返回true

 

return true;

 

L = L.__proto__;

 

}

 

}

 

####手动实现一个instanceof (用到constructor)

 

function instance_of(L, R) {//L 表示左表达式, R 表示右表达式

var O = R;

L = L.__proto__;

while (true) {

if (L === null)

return false;

if (O === L.constructor) // 这里重点: 当 O 严格等于 L 时, 返回 true

return true;

L = L.__proto__;

}

}

// 开始测试

var a = []

var b = {}

 

function Foo(){}

var c = new Foo()

 

function child(){}

function father(){}

child.prototype = new father()

var d = new child()

 

console.log(instance_of(a, Array)) // true

console.log(instance_of(b, Object)) // true

console.log(instance_of(b, Array)) // false

console.log(instance_of(a, Object)) // true

console.log(instance_of(c, Foo)) // true

console.log(instance_of(d, child)) // false 这里就是无法用于判断继承的

console.log(instance_of(d, father)) // true

 

####4. 实现继承的几种方式以及他们的优缺点

见: 理解原型设计模式以及JavaScript中的原型规则

####优缺点:

 

1. 原型链继承 -》缺点: 构造函数原型上的属性在所有该构造函数构造的实例上是共享的, 即属性没有私有化, 原型上属性的改变会作用到所有的实例上。

2. 构造函数继承 -》优缺点: 实现了属性的私有化, 但是子类无法访问父类原型上的属性。

3. 组合继承 -》 利用构造函数和原型链的方法, 可以比较完美的实现继承

 

function Super(){

 

this.flag = true;

 

}

Super.prototype.getFlag = function(){

 

return this.flag; //继承方法

 

}

function Sub(){

 

this.subFlag = flase

Super.call(this) //继承属性

 

}

Sub.prototype = new Super;

var obj = new Sub();

Sub.prototype.constructor = Sub;

Super.prototype.getSubFlag = function(){

 

return this.flag;

 

}

注:

 

这里还有个小问题, Sub.prototype = new Super; 会导致Sub.prototype的constructor指向Super; 然而constructor的定义是要指向原型属性对应的构造函数的, Sub.prototype是Sub构造函数的原型, 所以应该添加一句纠正: Sub.prototype.constructor = Sub;

 

4. 寄生继承 -》 即将sub.prototype=new super改为sub.prototype=Object.creat(supper.prototype), 避免了组合继承中构造函数调用了两次的弊端。

 

###5. 至少说出一种开源项目(如Node)中应用原型继承的案例

不理解

 

###6. 可以描述new一个对象的详细过程, 手动实现一个new操作符

 

####述new一个对象的详细过程:

 

使用new关键字调用函数(new ClassA(…))的具体步骤:

 

1. 创建空对象;

 

  var obj = {};

 

2. 设置新对象的constructor属性为构造函数的名称, 设置新对象的__proto__属性指向构造函数的prototype对象;

 

  obj.__proto__ = ClassA.prototype;

 

3. 使用新对象调用函数, 函数中的this被指向新实例对象:

 

  ClassA.call(obj);   //{}. 构造函数();

 

4. 将初始化完毕的新对象地址, 保存到等号左边的变量中

 

注意: 若构造函数中返回this或返回值是基本类型(number、 string、 boolean、 null、 undefined)的值, 则返回新实例对象; 若返回值是引用类型的值, 则实际返回值为这个引用类型。

 

var foo = "bar";

function test () {

  this.foo = "foo";

}

new test();         //test中的this指新对象, 并未改变全局的foo属性

console.log(this.foo); // "bar"

console.log(new test().foo); // "foo";

 

####手动实现一个new操作符:

 

第一种方法:

 

function new1(func) {

 

// 创建一个继承自func.prototype的新对象

var newObj = Object.create(func.prototype);

 

//截取new1函数第二个以及第二个之后的参数,在newObj作用域内执行改造函数func

var returnObj = func.apply(newObj, Array.prototype.slice.call(arguments, 1));

 

//如果传入参数中的构造函数执行后的returnObj是“对象”类型(比如new1(Object)),那么这个对象会取代newObj作为返回的对象

if ((typeof returnObj === "object" || typeof returnObj === "function") && ret !== null) {

return returnObj;

}

 

return newObj;

}

 

第二种方法:

 

function new2(func) {

 

return function() {

 

// 新生成一个对象,且新对象的原型对象继承自构造对象的原型对象

let newObj = {

__proto__: func.prototype

}

 

// 以第二次执行函数的参数,在obj作用域中执行func

var returnObj = func.apply(obj, arguments)

 

//同理,returnObj是“对象”类型(比如new1(Object)),那么这个对象会取代newObj作为返回的对象

if ((typeof returnObj === "object" || typeof returnObj === "function") && returnObj !== null) {

return returnObj;

}

 

return newObj

}

}

 

第三种方法:

 

function objectFactory() {

 

// 通过arguments类数组打印出的结果,我们可以看到其中包含了构造函数以及我们调用objectfactory时传入的其他参数

// 由于arguments是类数组,没有直接的方法可以供其使用,我们可以有以下两种方法:

// 1. Array.from(arguments).shift(); //转换成数组 使用数组的方法shift将第一项弹出

// 2.[].shift().call(arguments); // 通过call() 让arguments能够借用shift方法

 

let Constructor = [].shift.call(arguments);

 

const obj = new Object();

 

obj.__proto__ = Conctructor.prototype;

 

// 接下来的想法 给obj这个新生对象的原型指向它的构造函数的原型

// 给构造函数传入属性,注意:构造函数的this属性

// 参数传进Constructor对obj的属性赋值,this要指向obj对象

// 在Coustructor内部手动指定函数执行时的this 使用call、apply实现

Constructor.call(obj, ...arguments);

 

return obj;

}

 

###7. 理解es6 class构造以及继承的底层实现原理

 

####class构造:

Class本质上是一个特别的函数

Class 在语法上更加贴合面向对象的写法

Class 实现继承更加易读、 易理解

更易于写 java 等后端语言的使用

本质还是语法糖, 使用 prototype

 

es5:

function Fun(a, b) {

 

this.a = a;

this.b = b;

 

}

Fun.prototype.showA = function () {

 

console.log(this.a)

 

}

var fun = new Fun(1, 2);

fun.showA(); //1

 

es6则引用了class的概念, 使得更接近java、 c++等语言, 更加直观。 如:

class Fun {

 

constructor(a, b) {

this.a = a;

this.b = b;

}

showA() {

console.log(this.a);

}

 

}

var fun = new Fun(1, 2);

fun.showA(); //1

 

这两种写法是一样的, 在es6中, class可以理解为一个语法糖, 只是让这种写法更加直观。

要注意的是, es6中声明新的实例必须要用new声明。

其中constructor为类的默认方法, 通过new的调用可以执行这个方法。 每个类都必须要有这个方法, 如果没有显示定义,

则一个空的constructor被添加到类里面。 constructor方法默认返回实例对象, 即this。 也可以返回其他对象。 这事, 新的实例instanceof当前class就会报错。

 

####class继承的底层实现原理:

 

下面我们借助babel来探究es6类和继承的实现原理。

 

#####1. 类的实现

 

转换前:

 

class Parent {

constructor(a){

 

this.filed1 = a;

 

}

filed2 = 2;

func1 = function(){}

}

 

转换后:

function _classCallCheck(instance, Constructor) {

if (!(instance instanceof Constructor)) {

 

throw new TypeError("Cannot call a class as a function");

 

}

}

 

var Parent = function Parent(a) {

_classCallCheck(this, Parent);

 

this.filed2 = 2;

 

this.func1 = function () { };

 

this.filed1 = a;

};

 

解析:

 

1. 调用_classCallCheck方法判断当前函数调用前是否有new关键字。

 

构造函数执行前有new关键字, 会在构造函数内部创建一个空对象, 将构造函数的proptype指向这个空对象的_proto_, 并将this指向这个空对象。

如上, _classCallCheck中: this instanceof Parent 返回true。

若构造函数前面没有new则构造函数的proptype不会不出现在this的原型链上, 返回false。

 

2. 将class内部的变量和函数赋给this。

 

3. 执行constuctor内部的逻辑。

 

4.return this (构造函数默认在最后我们做了)。

 

######2. 继承实现

转换前:

class Child extends Parent {

 

constructor(a, b) {

super(a);

this.filed3 = b;

}

 

filed4 = 1;

func2 = function(){}

}

 

转换后:

 

function _inherits(subClass, superClass) {

 

// (1) 校验父构造函数。

// (2) 典型的寄生继承:用父类构造函数的proptype创建一个空对象,并将这个对象指向子类构造函数的proptype。

// (3) 将父构造函数指向子构造函数的_proto_(这步是做什么的不太明确,感觉没什么意义。)

 

if (typeof superClass !== "function" && superClass !== null) {

 

throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);

 

}

subClass.prototype = Object.create(superClass && superClass.prototype, {

 

constructor: {

value: subClass,

enumerable: false,

writable: true,

configurable: true

}

 

});

if (superClass){

 

Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;

 

}

}

 

function _possibleConstructorReturn(self, call) {

// 这里的Child.proto || Object.getPrototypeOf(Child)实际上是父构造函数(_inherits最后的操作),

// 然后通过call将其调用方改为当前this, 并传递参数。 (这里感觉可以直接用参数传过来的Parent)

// 校验this是否被初始化, super是否调用, 并返回父类已经赋值完的this。

if (!self) {

 

throw new ReferenceError("this hasn't been initialised - super() hasn't been called");

 

}

return call && (typeof call === "object" || typeof call === "function") ? call : self;

}

 

var Child = function (_Parent) {

_inherits(Child, _Parent);

 

function Child(a, b) {

 

_classCallCheck(this, Child);

 

var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a));

 

_this.filed4 = 1;

 

_this.func2 = function() {};

 

_this.filed3 = b;

return _this;

 

}

 

return Child;

}(Parent);

 

以上代码做了:

 

1. 调用_inherits函数继承父类的proptype。

2. 用一个闭包保存父类引用, 在闭包内部做子类构造逻辑。

3. new检查。

4. 用当前this调用父类构造函数。

5. 将行子类class内部的变量和函数赋给this。

6. 执行子类constuctor内部的逻辑。

 

可见, es6实际上是为我们提供了一个“组合寄生继承”的简单写法。

 

你可能感兴趣的:(javascript,js基础,前端,自检)