为什么我们需要函数原型

更新日期: 2022-05-07阅读: 949标签: 函数

想要了解函数原型,就需要从JavaScript的对象继承模式说起(对象的继承:A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的)

大部分面对对象编程语言,如Java,其对象的继承基本都是通过类(class)来实现,而JavaScript则采用了原型对象(prototype)来得以实现

原型继承:构造函数的实例对象自动拥有构造函数原型对象的属性和方法(利用原型链),这正是JavaScript实现继承的方法

ES6 引入了 class 语法,但此处先不做介绍,所以在JavaScript中,想要创建对象,有如下几种方法

对象创建方法

//方法一:Object构造函数
//适用场景:起始时不确定对象内部数据
var p = new Object()
p.name = 'jack'
p.age = 18
p.setName = function(name){
    this.name = name
}

//方法二:对象字面量模式
//适用场景:起始时对象内部数据确定
//多个相同对象时重复度高
var p = {
    name: 'jack'
    age: 18
    setName:function(name){
        this.name = name
    }
}

//方法三:工厂函数动态创建对象并返回
//适用场景:创建同样的多个对象
//缺点:对象没有具体的类型,都是Object类型
function createPerson(name,age){
    var p = {
        name: name
        age:age
        setName:function(name){
            this.name = name
        }
    }
    return p
}

//方法四:自定义构造函数
//适用场景:创建多个类型确定的对象
//每个对象都有相同的数据,浪费内存
function Person(name,age){
    this.name=name;
    this.age=age;
    this.setName = function(name){
        this.name = name
    }
}
  • 前两种方法都不能用于批量创建对象
  • 工厂函数和构造函数区别:

    • 创建实例化对象后,工厂函数实例化对象的constructor是Object,构造函数实例化对象的constructor是构造函数本身
    • 构造函数内部this是构造函数本身,并可以被继承到实例化对象上,工厂函数this指向window
    • 工厂函数和构造函数本身并没有区别(只是人为的喜欢将构造函数首字母大写),是因为new创建实例时产生的区别,稍后一点会详细讲解constructor和new

prototype

所有函数都有prototype属性,此处使用构造函数来进行解释

(在原型这一部分最好自己打开控制台调试,并且认真区分每个属性是函数还是对象)

function Person(name){
    this.name=name;
    this.sayHi = function(){
        console.log('hi')
    }
}

var person1 = new Person('jack')
var person2 = new Person('rose')
person1.sayHi === person2.sayHi //false

由此我们可以看出:虽然都是同一个方法,但是实例化对象不同,导致他们并不相同,而是各自保存了自己的sayHi函数(具体原理在new中讲解),这就导致了内存的浪费

而为了解决这种内存的浪费,JavaScript就提出了原型(prototype)这个概念

既然每个实例化对象的相同方法或属性不同,那我们就为构造函数创建一个新的对象,名为prototype,专用于保存这种可以共享的属性或方法

//于是上面的Person构造函数修改为
function Person(name){
    this.name=name;
}
Person.prototype.sayHi = function(){
        console.log('hi')
    }

//这样我们就可以通过原型来访问sayHi函数
var person1 = new Person('jack')
var person2 = new Person('rose')
person1.sayHi === person2.sayHi //true
//至于为什么person1就可以直接访问到sayHi函数,将在constructor中解释

//此时如果我们console.log(person1)可以得到
|--Person{name:'jack'}
 |--name: "jack"
 |--[[prototype]]:Object
  |--sayHi f()
  |--constructor: f Person(name)
  |--[[prototype]]: Object
//可以看到此时Person的prototype就有了sayHi函数,下面的prototype是Object的原型,将在原型链中说到

constructor

所有对象都会从它的原型上继承一个 constructor 属性,用于指向此对象的构造函数,constructor 属性即位于原型prototype上

function Person(){
   
}
var p = new Person()
console.log(p)// 打印出的p是一个Person对象
|--Person{}
 |--[[prototype]]:Object
  |--constructor: f Person()
  |--[[prototype]]: Object
//可以看到这个Person对象中就有一个prototype属性,而prototype中就保存有constructor,并且值为构造函数f Person()

所以我们为什么说是从原型上继承了constructor属性,因为p本身是没有这个属性,而是读取的构造函数上的原型的constructor 属性,即

p.constructor === Person // true
p.constructor === Person.prototype.constructor // true
p.hasOwnProperty('constructor') // false

//一般情况下,constructor用于索引对象的原型
p.constructor.prototype

然后回到之前prototype中为什么person1.sayHi === person2.sayHi,其实可以理解为

person1.constructor.prototype.sayHi === person2.constructor.prototype.sayHi
//person1.constructor === Person

原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法

这样的解释虽然很正确,但也非常官方,导致初学者可能难以理解

简单来说,JavaScript中每个函数都有一个原型prototype(其实对象也有,会在原型链原理中说到),用于索引其更上级的对象和函数

function Person(){
}
var p = new Person()
var obj = new Object()

Person.prototype.x = 1;
Object.prototype.y = 2;

console.log(p)
//此时形成的Person如下
|--Person{}
 |--[[prototype]]:Object
  |--x:1
  |--constructor: f Person()
  |--[[prototype]]: Object
   |--y:2
   |--........
//我们可以看到,之前例子中未知的prototype其实是Object的原型
//而如果有构造函数Person2以Person为原型构造,则Person的prototype又是这个Person2的prototype上的prototype
//由此,原型之间不断的连接形成了一种链式关系,就称为原型链

console.log(p.y)
//我们可以通过原型链直接去访问上层原型的属性或方法,如这里可以直接用p.y
//这也是为什么我们可以直接用valueOf()等函数的原因,因为valueOf()位于Object的原型上

原型链有一些性质是我们必须记住的:

  • 原型链按顺序依次查找,如果上例中,有Person.prototype.y=3,则结果为3,因为找到后就不会再顺着原型链往上查找
  • 如果一层层地上溯,所有对象的原型都为Object。也就是说,所有对象都继承了Object.prototype的属性
  • 而Object的原型是null,因此,原型链的尽头就是null

原型链原理

关于原型链原理,其中的_proto_属性已经即将废除,但是目前还没有更好的方法来解释原型链原理,所以这部分也可以不用学习透彻,大概了解就行,实际上已经不会用到,但是对原型链的原理理解十分重要。

原型链实际上通过隐式原型查找,所以也成为隐式原型链

在实际的JavaScript工作中,并不是只有函数有prototype,每个实例化对象也会有一个隐式的prototype,为了区分,命名为_proto_

_proto_属性与prototype作用一样,都指向了构造函数的原型,只是_proto_是用于实例化对象

  • 每个构造函数都有一个prototype属性,默认指向一个空对象(除了Object构造函数),即显式原型
  • 每个实例对象都有一个_proto_属性,称为隐式原型
  • 隐式原型的值为其对应构造函数的显式原型的值
  • 实例对象中有一个constructor属性,指向构造函数
  • 构造函数和原型对象相互引用
//上面的写成具体例子如下
function Fn(){
    //默认this.prototype = {}
}
var f = new Fn()//内部语句f._proto_ = Fn.prototype
//无法直接访问到f._proto_

f._proto_ === Fn.prototype // true
f.constructor === Fn === Fn.prototype.constructor // true

//实际上原型链就变为
Fn.prototype._proto_ === Object.prototype._proto_//这里Object是Fn的上层原型,不是具体指Object构造函数

所以实际上访问一个对象的属性的顺序为:

  • 先在自身属性中查找
  • 沿着_proto_链往上找,找到返回
  • 没找到就返回undefined

原型链还有如下几条性质:

  • function会new自身

    var Function = new Function()
  • Object并不是原型链顶层,Object.prototype才是原型链的顶层,有:Object.prototype._proto_ = null
  • 所有函数都是Function的实例(包括Function),有:Function.prototype = Function._proto_
  • 所有对象都是Object的实例(包括原型对象),有:Function.prototype._proto_ = Object.prototype
  • 所有函数的_proto_属性都是一样,因为所有Function都new了自身

上图即为原型链完整原理,可以看出,确实十分的繁杂,所以目前基本只讨论prototype属性,而将_proto_封装起来

new

在前文所讲时,我都是用构造函数来举例,实际上普通函数和构造函数都有prototype,而造成普通函数和构造函数区别的,也正是new调用时,而不是函数创建时的写法

new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性_proto_,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

如果不用_proto_,就解释为:

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部的this关键字。
  4. 开始执行构造函数内部的代码
//可以简单理解为如下代码
function New(){
    
}
var f = new New(){
    var this = {}
    this._proto_ = Fn.prototype
    //执行函数体内容
    
    return this
}
//如果构造函数return了其他的对象,则new命令会返回这个新对象
//如果构造函数return其他值,则不会返回,仍然返回this
//实际上new的实现代码会更为复杂
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
  // 将 arguments 对象转为数组
  var args = [].slice.call(arguments);
  // 取出构造函数
  var constructor = args.shift();
  // 创建一个空对象,继承构造函数的 prototype 属性
  var context = Object.create(constructor.prototype);
  // 执行构造函数
  var result = constructor.apply(context, args);
  // 如果返回结果是对象,就直接返回,否则返回 context 对象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 实例
var actor = _new(Person, '张三', 28);
原文来源:为什么我们需要函数原型 - Sheldon's blog (drsheldon-zh.com)


链接: https://fly63.com/article/detial/11461

JavaScript 函数式编程

我理解的 JavaScript 函数式编程,都认为属于函数式编程的范畴,只要他们是以函数作为主要载体的。

Js函数式编程,给你的代码增加一点点函数式编程的特性

给你的代码增加一点点函数式编程的特性,最近我对函数式编程非常感兴趣。这个概念让我着迷:应用数学来增强抽象性和强制纯粹性,以避免副作用,并实现代码的良好可复用性。同时,函数式编程非常复杂。

让我们来创建一个JavaScript Wait函数

Async/await以及它底层promises的应用正在猛烈地冲击着JS的世界。在大多数客户端和JS服务端平台的支持下,回调编程已经成为过去的事情。当然,基于回调的编程很丑陋的。

JavaScript函数创建的细节

如果你曾经了解或编写过JavaScript,你可能已经注意到定义函数的方法有两种。即便是对编程语言有更多经验的人也很难理解这些差异。在这篇博客的第一部分,我们将深入探讨函数声明和函数表达式之间的差异。

编写小而美函数的艺术

随着软件应用的复杂度不断上升,为了确保应用稳定且易拓展,代码质量就变的越来越重要。不幸的是,包括我在内的几乎每个开发者在职业生涯中都会面对质量很差的代码。这些代码通常有以下特征:

javascript回调函数的理解和使用方法(callback)

在js开发中,程序代码是从上而下一条线执行的,但有时候我们需要等待一个操作结束后,再进行下一步操作,这个时候就需要用到回调函数。 在js中,函数也是对象,确切地说:函数是用Function()构造函数创建的Function对象。

js调用函数的几种方法_ES5/ES6的函数调用方式

这篇文章主要介绍ES5中函数的4种调用,在ES5中函数内容的this指向和调用方法有关。以及ES6中函数的调用,使用箭头函数,其中箭头函数的this是和定义时有关和调用无关。

JavaScript中函数的三种定义方法

函数的三种定义方法分别是:函数定义语句、函数直接量表达式和Function()构造函数的方法,下面依次介绍这几种方法具体怎么实现,在实际编程中,Function()构造函数很少用到,前两中定义方法使用比较普遍。

js在excel的编写_excel支持使用JavaScript自定义函数编写

微软 称excel就实现面向开发者的功能,也就是说我们不仅可以全新定义的公式,还可以重新定义excel的内置函数,现在Excel自定义函数增加了使用 JavaScript 编写的支持,下面就简单介绍下如何使用js来编写excel自定义函数。

js中的立即执行函数的写法,立即执行函数作用是什么?

这篇文章主要讲解:js立即执行函数是什么?js使用立即执行函数有什么作用呢?js立即执行函数的写法有哪些?

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!