浅析JavaScript中原型及constructorprotoprototype的关系

前言先说一说为什么要搞清楚JavaScript的原型,因为这就是JS的根。JavaScript虽然不是一门传统的面向对象语言,但它有自己的类和继承机制,最重要的就是它采用了原型的概念。与其说JS是面向对象,不如叫面向原型。JS这门语言从开发之初就是基于原型去做事情的,它是面向对象的思想,但归根结底是面向原型的原理,从操作上来说也是这样的。 我们老师以前说过,好多工作几年的人,在这个问题上都模棱两可。基础才会是决定一个程序员上限的最终指标。因为对一门语言的基础掌握得越好,就越可能通过原生的语言去开发新的东西,框架也好、插件也好。但如果基础不好,顶多也就能用用别人开发的东西,你自己是没能力去开发的。 那么要搞懂原型,涉及到的知识点就比较多了,构造函数、指针、对象、继承这些概念都会是门槛,但不要着急,一口是没法吃成大胖子的。我在翻阅研究《JavaScript高编》和各种资料后,总结出了自己对这部分的理解,尽量用连贯性和通俗易懂的方法去解释,这样方便自己的记忆,相信大家看了也不会懵逼。 原型模式原型的概念那么到底什么是原型呢?原型的英文就是“prototype”。记住这个朋友,它会伴随我们的一生,不离不弃。 维基百科的官方解释:原型模式是是面向对象编程的子系统和一种方式,特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”,这个原型是可定制的。维基百科:https://zh.wikipedia.org/wiki... 通俗地讲,原型就是我们复印件的原件。 那么JS里的原型是什么呢?“我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。”原型就是这句话中的prototype,说白了它就是一个对象{},叫原型对象。 抓重点:1.每个函数才有prototype属性,即function abc(){},abc.prototype是存在的!2.prototype是一个指针,指向一个对象。(指针什么意思?下面解答)3.prototype指向的对象可以让其他实例共享其属性和方法。 这里的第二点,指针到底是个什么意思呢?比如var a = [1],指针就是指向等号右边这个数组在计算机内存中的存储地址。所以当我们使用a的时候,a就是一个指针,它指向[1]存储的地方。说得通俗点,就是通过a这个变量,我们可以找到等号右边这个值。 prototype就是指向一个对象存储的地方,可以理解为我们去找到prototype = {}中等号右边这个值,所以最终返回的就是一个对象。 原型模式和构造函数模式的区别但是我们在JS中创建新的对象会有两种模式,一种是构造函数模式,一种就是原型模式。这个构造函数也是new一个实例,他们有什么区别呢?先看下面这段代码: /*原型模式*/function Person(){}; //新建一个空函数Person.prototype.name = 'Gomi'; //为它的原型添加属性Person.prototype.myName = function(){ console.log(this.name)};var gomi = new Person(); //复制一个Person实例var gomi2 = new Person(); //再复制一个Person实例gomi.myName === gomi2.myName //true,说明他们的this都是指向同一个原型,即Person.prototype。/*构造函数模式*/function Animal(){ this.name = 'cat', this.myName = function(){ console.log(this.name) }}; //新建一个函数,并直接在里面添加属性和值var jack = new Animal(); //同样实例化了一遍,但这时每个实例中的属性和值都是独立存在的。var jack2 = new Animal(); jack.myName === jack2.myName //false,说明他们的this指向不同,都是单独生成的新的实例,而不是依赖于同一个原型。原型和构造函数的区别就像影分身和克隆的区别。我们把原型模式看作影分身,复制原型的过程看作本体产生分身的过程,影分身的任何动作都是基于那个唯一本体的,他做什么,影分身就会做什么。而构造函数模式就是克隆,虽然克隆的时候是基于唯一本体的基因,但其实克隆出来的每个人都是一个新的独立的人了,他们虽然长得一模一样,但互相之间没有任何关联。如果本体整容了,其余的克隆人也不会变。但在本体整容后再进行克隆的人,肯定就会跟整容后一样咯。而影分身是,一旦本体整容,那么所有分身都会跟着变样。 搞清了构造函数和原型的区别后,就可以继续了。 constructor、prototype、__proto__的关系光是搞清楚构造函数和原型的区别还远远不够,我们经常会在控制台看到下面这种结构: 这是一个绝对能够搞晕你的结构,我圈出的constructor、prototype、__proto__这三者总是在出现,总是在互相嵌套。他们到底是什么关系?又代表什么意思呢? ...

May 24, 2019 · 1 min · jiezi

关于构造函数、原型、原型链、多种方式继承

构造函数与实例对象又是这个经典的问题,嗯,我先来写个构造函数,然后实例化一个对象看看。function Person(name) { this.name = name}Person.prototype.eat = () => {console.log(’eat’)}Person.prototype.play = () => {console.log(‘play’)}let Han = new Person(‘Han’)通过一系列打印发现了这样的关系:原型链 – 原型(prototype)和隐式原型(proto)可以看出实例对象没有prototype(也就是原型),只有构造器才拥有原型。而所有的js对象都拥有__proto__(也就是隐式原型),这个隐式原型所指向的就是创造这个对象的构造器的原型。如实例Han的隐式原型指向了其构造函数(Person)的原型;Person的隐式原型指向了Function的原型;而原型自身也有隐式原型,指向了Object的原型。有点绕口,其实就是通过隐式原型可以向上找到是谁构造了自己,并且如果自己没有相应的属性或者方法,可以沿着这条原型链向上找到最近的一个属性或方法来调用。如Han.eat(),实际上是调用了Han.proto.eat(),把构造器Person的原型的eat方法给拿来用了;再如Han.hasOwnProperty(’name’),实际上是调用了Han.proto.proto.hasOwnProperty(’name’),因为Han自己没hasOwnProperty这方法,就通过隐式原型向上找到了Person的原型,发现也没这方法,就只能再沿着Person的原型的隐式原型向上找到了Object的原型,嗯然后发现有这方法就拿来调用了。构造器constructor所有构造函数都有自己的原型(prototype),而原型一定有constructor这么个属性,指向构造函数本身。也就是告诉大家这个原型是属于本构造函数的。Function & Object可以看出Person这个构造函数是由Function创建出来的,而我们看下Function的隐式原型,竟然是指向了Function的原型,也就是Function也是由Function创建出来的。很绕是不是,我们先不管,继续溯源下去,再看下Function的原型的隐式原型,指向的是Object的原型,继续往上找Object的原型的隐式原型,嗯终于结束了找到的是null,也就是Object的原型是原型链上的最后一个元素了。接下来看下Object,Object是由Function创建出来的,而Function的隐式原型的隐式原型是Object的原型也就是Function通过原型链可以向上找到Object的原型,两者看起来是你生我我生你的关系,这里也就引用比较好懂的文章来解释下: 从Object和Function说说JS的原型链ObjectJavaScript中的所有对象都来自Object;所有对象从Object.prototype继承方法和属性,尽管它们可能被覆盖。例如,其他构造函数的原型将覆盖constructor属性并提供自己的toString()方法。Object原型对象的更改将传播到所有对象,除非受到这些更改的属性和方法将沿原型链进一步覆盖。FunctionFunction 构造函数 创建一个新的Function对象。 在 JavaScript 中, 每个函数实际上都是一个Function对象。—- 来自mozilla接下来说下构造函数实例化对象到底做了些啥,其实看也能看出来了。let Jan = {}Person.call(Jan, ‘Jan’)Jan.proto = Person.prototype1、创建一个空对象。2、将构造函数的执行对象this赋给这个空对象并且执行。3、把对象的隐式原型指向构造函数的原型。4、返回这个对象是的就是这样,next page!继承原型链继承function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss() {}Boss.prototype = new Person()let Han = new Boss()原理就是这样????子构造函数的原型指向了父构造函数的实例对象,因此子构造函数的实例对象可以通过原型链找到父构造函数的原型方法和类属性。优点:所有实例对象都可以共享父构造函数的原型方法。缺点:1、父构造函数的引用属性也被共享了,相当于所有的实例对象只要对自身的skills属性进行修改都会引发共振,因为其实修改的是原型链上的skills属性。当然对skills重新赋值可以摆脱这一枷锁,相当于自身新建了skills属性来覆盖了原型链上的。2、实例化时无法对父构造函数传参。3、子构造函数原型中的constructor不再是子类自身,而是通过原型链找到了父类的constructor。构造函数继承function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss(name) { Person.call(this, name)}let Han = new Boss(‘Han’)原理就是父构造函数把执行对象赋给子构造函数的实例对象后执行自身。优点:1、实例化时可以对父构造函数传参。2、父类的引用属性不会被共享。3、子构造函数原型中的constructor还是自身,原型没有被修改。缺点:每次实例化都执行了一次父构造函数,子类不能继承父类的原型,如果把父类原型上的方法写在父类的构造函数里,虽然子类实例对象可以调用父类的方法,但父类的方法是单独加在每个实例对象上,会造成性能的浪费。组合继承结合了原型链继承和构造函数继承两种方法。function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss(name, age) { Person.call(this, name) this.age = age}Boss.prototype = new Person()Boss.prototype.constructor = BossBoss.prototype.sleep = ()=> {console.log(‘sleep’)}let Han = new Boss(‘Han’, 18)看起来是完美解决了一切。但就是????实例化的对象实际上是用构造函数继承的方法往自己身上加属性从而覆盖原型链上的相应属性的,既然如此,为什么不直接那父构造器的原型加到子构造器的原型上呢?这样就不会出现那多余的父类实例化对象出来的属性了。function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss(name, age) { Person.call(this, name) this.age = age}Boss.prototype = Person.prototype //modifiedBoss.prototype.constructor = BossBoss.prototype.sleep = ()=> {console.log(‘sleep’)}let Han = new Boss(‘Han’, 18)看起来很是完美,反正效果是达到了,性能优化也是最佳。但问题就是这样一点继承关系都看不出来啊,父类和子类的原型完全融合在一块了,一点都不严谨。所以最优的继承方式应该是。。。寄生组合继承function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss(name, age) { Person.call(this, name) this.age = age}Boss.prototype = Object.create(Person.prototype)Boss.prototype.sleep = ()=> {console.log(‘sleep’)}Boss.prototype.constructor = Bosslet Han = new Boss(‘Han’, 18)先看图????其实跟组合继承有点像,构造函数继承部分和组合继承的一样就不说了。原型链那块和原型链继承有所不同的是原型链继承是直接拿了父类的实例对象来作为子类的原型,而这里是用以父类的原型为原型的构造函数实例化出来的对象作为子类的原型(Object.create做的事情),完美避开了不必要的父类构造函数里的东西。Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。相当于这样????function create(proto) { function F() {} F.prototype = proto return new F()}听说ES6的class extend也是这么做的,更多的继承细节可以看看这篇文章,本继承章节也参考了的????一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends ...

March 16, 2019 · 1 min · jiezi

javascript 面向对象 new 关键字 原型链 构造函数

JavaScript面向对象JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构构造函数的首字母大写,区分一般函数。函数体内部使用了this关键字,代表了所要生成的对象实例。生成对象的时候,必须使用new命令。构造函数内部使用严格模式 ‘use strict’,防止当做一般函数调用,这样就会报错。function Person(name, age, sex) { ‘use strict’; this.name = name; this.age = age; this.sex = sex;}Person() 报错new Person(“zhangxc”, 29, “male”);1、new关键字 命令内部实现function _new(constructor, params) { // 接受个数不确定参数,第一个参数:构造函数;第二个到第n个参数:构造函数传递的参数。 // 1. 首先将参数组成一个数组 // 首先 .slice 这个方法在不接受任何参数的时候会返回 this 本身,这是一个 Array.prototype 下的方法,因此 this 就是指向调用 .slice 方法的数组本身。 var args = Array.prototype.slice.call(arguments); // arguments伪数组,获取函数的所有参数的伪数组。 // 等价于 // [].slice.call(arguments); // 2. 获取构造函数 var constructor = args.shift(); // shift()返回数组第一个元素 // 3. 使用构造函数原型创建一个对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()方法。 var context = Object.create(constructor.prototype); // Object.create()参数是一个对象,新建的对象继承参数对象的所有属性 // 4. 将参数属性附加到对象上面 var result = constructor.apply(context, args); // 5. 返回一个对象 return (typeof result === ‘object’ && result != null) ? result : context;}function Person(name, age, sex) { this.name = name; this.age = age; this.sex = sex;}var args1 = _new(Person, “zhangxc”, 18, “male”);// {name: “zhangxc”, age: 18, sex: “male”}var args2 = new Person(“zhangxc”, 18, “male”);// {name: “zhangxc”, age: 18, sex: “male”}new.target属性如果当前函数是new命令调用,new.target指向当前函数(构造函数的名称),否则为undefined。function Test() { console.log(new.target === Test);}Test() // falsenew Test() // true2、this关键字…3、对象的继承… 待完善 ...

February 13, 2019 · 1 min · jiezi