函数=》构造函数=》对象=》原型与原型链=》类

函数

1. 理解特性

  • 通常来讲,一个函数是能够经过外部代码调用的一个“子程序”(或在递归的状况下由内部函数调用)。
  • 在 JavaScript中,函数是头等(first-class)对象,由于它们能够像任何其余对象同样具备属性和方法,也能够赋值给变量或做为参数传递给其它函数。(它们与其余对象的区别在于函数能够被调用。简而言之,它们是Function对象。)
  • 若是一个函数中没有使用return语句,则它默认返回undefined。
  • 调用函数时,传递给函数的值被称为函数的实参(值传递),对应位置的函数参数名叫做形参。(若是实参是一个包含原始值(数字,字符串,布尔值)的变量,内部改变对应形参的值,返回后,该实参变量的值也不会改变。若是实参是一个对象引用,则对应形参会和该实参指向同一个对象。假如函数在内部改变了对应形参的值,返回后,实参指向的对象的值也会改变),因此这里常常会有一个深浅拷贝问题
  • 每次调用时还有拥有另外一个值---本次调用的上下文---这就是this关键字的值
  • 当函数嵌套在其它函数中定义,这时它就能够访问它被定义时所处的做用域中的任何变量(这一位着javascript函数构成一个闭包)。

2. 函数定义

2.1. 函数声明 (函数语句)

function name([param[, param[, ... param]]]) {
    statements
}
// name 函数名.
// param 传递给函数的参数的名称,一个函数最多能够有255个参数.
// statements 组成函数体的声明语句.
复制代码

函数声明语句“被提早”到外部脚本或外部函数做用域的顶部,因此能够被在它定义以前出现的代码所调用。javascript

2.2. 函数表达式

var myFunction = function name([param[, param[, ... param]]]) {
    statements
}
// name 函数名,能够省略。当省略函数名的时候,该函数就成为了匿名函数。
// param 与 statements的做用和函数声明中同样。
复制代码

函数表达式不会提高,因此不能在定义以前调用。java

2.3. 箭头函数表达式

([param] [, param]) => { statements }

param => expression

// param 参数名称. 零参数须要用()表示. 只有一个参数时不须要括号. (例如 foo => 1)
// statements or expression 多个声明statements须要用大括号括起来,而单个表达式时则不须要。表达式expression也是该函数的隐式返回值。
复制代码

3.函数属性和方法

由于函数是javascript中的特殊对象,因此它们也能够拥有属性和方法。程序员

3.1. length属性

函数的length属性是只读属性,他表明函数实参的数量。es6

3.2. prototype属性

  • 每个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象叫‘原型对象’(prototype object)。
  • 每个函数都包含不一样的原型对象。
  • 当将函数用做构造函数的时候,新建立的对象会从原型对象上继承属性。

3.3. call()方法和apply方法()

了解更多请看这篇文章 juejin.im/post/5e6afa…express

3.4. bind()方法

同上编程

3.4. toString()方法

方法返回一个字符串,这个字符串和函数声明语句的语法有关,大多数(非所有)的toString()方法的实现都返回函数的完整源码。浏览器

构造函数(constructor)

所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。闭包

构造函数就是一个普通的函数app

var Animal = function () {
  this.name = 'dog';
};
复制代码

上面代码中,Animal就是构造函数。为了与普通函数区别,构造函数名字的第一个字母一般大写。ide

构造函数的特色有两个。

  • 函数体内部使用了this关键字,表明了所要生成的对象实例。
  • 生成对象的时候,必须使用new命令。

1. new命令

1.1. 做用

new命令的做用,就是执行构造函数,返回一个实例对象。

var Animal = function () {
  this.name = 'dog';
};

var v = new Animal();
v.name // dog
复制代码

解析:

  • 上面代码经过new命令,让构造函数Animal生成一个实例对象,保存在变量v中。这个新生成的实例对象,从构造函数Animal中获得了name属性。
  • new命令执行时,构造函数内部的this,就表明了新生成的实例对象,this.name表示实例对象有一个name属性,值是dog。

若是忘了使用new命令,直接调用构造函数时,构造函数就变成了普通函数,并不会生成实例对象。

1.2. new 命令的原理

使用new命令时,它后面的函数依次执行下面的步骤。

  • 1.建立一个空对象,做为将要返回的对象实例。
  • 2.将这个空对象的原型,指向构造函数的prototype属性。
  • 3.将这个空对象赋值给函数内部的this关键字。
  • 4.开始执行构造函数内部的代码。

解析: 也就是说,构造函数内部,this指的是一个新生成的空对象,全部针对this的操做,都会发生在这个空对象上。构造函数之因此叫“构造函数”,就是说这个函数的目的,就是操做一个空对象(即this对象),将其“构造”为须要的样子。

function _new() {
    var obj = new Object(); // 1.建立一个空对象,做为将要返回的对象实例。
    var Constructor = [].shift.call(arguments); // 取出构造函数。
    obj.__proto__ = Constructor.prototype; // 2.将这个空对象的原型,指向构造函数的prototype属性。
    var ret = Constructor.apply(obj, arguments); // 3.将这个空对象赋值给函数内部的this关键字。4.开始执行构造函数内部的代码。
    return typeof ret === 'object' ? ret : obj;
}
复制代码

对象

JavaScript 的设计是一个简单的基于对象的范式。一个对象就是一系列属性的集合,一个属性包含一个名和一个值。一个属性的值能够是函数,这种状况下属性也被称为方法。除了浏览器里面预约义的那些对象以外,你也能够定义你本身的对象。

对象基础知识请看这三篇文章

1.使用构造函数创造对象

  • 经过建立一个构造函数来定义对象的类型。
  • 经过 new 建立对象实例。

为了定义对象类型,为对象类型建立一个函数以声明类型的名称、属性和方法。例如,你想为汽车建立一个类型,而且将这类对象称为 car ,而且拥有属性 make, model, 和 year,你能够建立以下的函数:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
// 注意经过使用 this 将传入函数的值赋给对象的属性。
复制代码

你能够经过调用 new 建立任意数量的 car 对象。例如:

var kenscar = new Car("Nissan", "300ZX", 1992);
var vpgscar = new Car("Mazda", "Miata", 1990);
// 这就是上文讨论的构造函数的做用
复制代码

2. Object.create() 方法

对象也能够用 Object.create()方法建立。该方法很是有用,由于它容许你为建立的对象选择一个原型对象,而不用定义构造函数。

var Animal = {
  type: "Invertebrates", // 属性默认值
  displayType : function() {  // 用于显示type属性的方法
    console.log(this.type);
  }
}

// 建立一种新的动物——animal1 
var animal1 = Object.create(Animal); // Animal 是一个普通对象,不是构造函数,咱们也能创造出新动物。
animal1.displayType(); // Invertebrates
复制代码

原型prototype与原型链(继承)

每个javascript对象(null除外)都和另外一个对象相关联。另外一个对象就是咱们所说的原型,每个对象都从原型继承属性。

  • 经过对象直接量建立的对象都具备同一个原型对象,并能够经过Object.prototype得到原型对象的引用。
  • 经过new和构造函数调用建立的对象的原型就是构造函数的prototype属性的值。
  • 经过new Object()建立的对象也继承自Object.prototype。
  • 相似的,经过new Array()建立的对象的原型就是Array.prototype;经过new Date()建立的对象的原型就是Date.prototype。
  • 内置的构造函数(Array,Date等)都具备一个继承自Object.prototype的原型,因此由new Date()建立的Date对象的属性同时继承自Date.prototype和Object.prototype。这就是链式继承,叫‘原型链’(prototype chain)。

每一个实例对象( object )都有一个私有属性(称之为 _ _ proto _ _)指向它的构造函数的原型对象(prototype )。该原型对象也有一个本身的原型对象( _ _ proto _ _ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并做为这个原型链中的最后一个环节。

1.函数原型prototype

从上文可知,全部的函数会有一个特别的属性 —— prototype

1.1 不妨让咱们打开浏览器F12,查看控制台,输入如下代码

function wqh(){};
console.log( wqh.prototype );
复制代码

能够看到输出的prototype是一个对象,有两个属性constructor和 _ _ proto _ _

让咱们点开 _ _ proto _ _ 查看更多内容

由上文咱们知道对象的_ _ proto _ _指向他的它的构造函数的原型对象(prototype),查看截图咱们知道,这里指向了Object原型

1.2 如今让咱们给a函数的原型对象(prototype),添加一个新属性

function wqh(){};
wqh.prototype.age = 18
console.log( wqh.prototype );
复制代码

让咱们来查看输出结果

1.3 使用new操做符构造出实例对象

function wqh(){};
wqh.prototype.age = 18;
var good = new wqh();
good.height = 178;
console.log( good.age );
console.log( good );
复制代码

让咱们来查看输出结果

可知,new出来的实例对象的_ _ proto _ _属性与wqh构造函数的prototype属性如出一辙。因此实例对象的 _ _ proto _ _ 指向它的构造函数的原型对象(prototype )

这一层一层错的_ _ proto _ _就是原型链。当实例上没有age属性时,他会去 _ _ proto _ _原型上查找,若是尚未会继续往上层查找,这个过程就是原型链查找。

2.原型链(继承)

根据上文讨论,画图得

因此当咱们写构造函数时,若是想写一些公共的(可继承的)属性或方法,能够写在prototype原型上。

还有个好处是能够节省内存;例如,在构造函数上写了一个方法,若是new了100个实例,那这个方法将会被构造生成100次,若是将这个方法写在构造函数的原型上,那它只会构造生成一次,new出的实例会在原型链上查找这个方法,大大节省内存。

3.真的是继承吗?

继承这个概念术语我最先是从java里接触到的,但咱们js里的这个原型链是继承吗?

引用《你不知道的JavaScript》中的话:继承意味着复制操做,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间建立一个关联,这样,一个对象就能够经过委托访问另外一个对象的属性和函数,因此与其叫继承,委托的说法反而更准确些。

但咱们通常仍是会说继承,由于从理解层面来讲意思是同样的,这样跟非javascript程序员就能够正常交流,和同门的js程序员交流必定要讲原型链和原型链查找,这样更准确。

类的概念:一组具备相同属性和行为的对象的抽象。Javascipt语法(es6前)不支持"类"(class)

  • 经过前文咱们知道JavaScript 使用原型来‘继承’:每一个对象都从其原型对象‘继承’属性和方法。
  • 在 JavaScript 中不存在 Java 等语言中所使用的做为建立对象 蓝图的传统类,原型‘继承’仅处理对象。
  • 但咱们在使用的时候发现,原型‘继承’能够模仿经典类的继承。为了将传统类引入 JavaScript,ES2015 标准(es6)引入了 class 语法:基于原型'继承'的语法糖。

因此总结来讲:js(es6后)中的类仍是原型和基于原型链的‘继承’来写的构造函数。只是为了语法简洁,通用,便于理解(理解类比理解原型链简单多了)封装的语法。

1.构造函数写法

// 定义构造函数
// 这里遵循一个常见的编程约定:从某种意义上讲,定义构造函数既是定义类,而且类名首字母要大写。
function People(name, age) {
    this.name = name;
    this.age = age;
};
People.prototype.showName = function () {
    console.log(this.name);
};

// 使用构造函数
var p1 = new People('wqh', 18);
p1.showName();
var p2 = new People('w', 19);
var p3 = new People('q', 20);
console.log(p1, p2, p3);
复制代码

查看输出结果咱们发现:

  • new出的不一样实例的_ _ proto _ _都指向了同一个原型。
  • 原型上的constructor都指向了同一个构造函数People。
  • People构造函数prototype原型上写的方法,被实例对象‘继承’了。
  • 只有写在prototype原型上的属性才会被‘继承’(共有属性),其它地方是私有属性。

补充一个易错点

function People(name, age) {
    this.name = name;
    this.age = age;
};
People.prototype = {
    showName: function () {
        console.log(this.name);
    }
}

// 使用构造函数
var p1 = new People('wqh', 18);
p1.showName();
var p2 = new People('w', 19);
var p3 = new People('q', 20);
console.log(p1, p2, p3);
复制代码

咱们发现当prototype重定义为一个对象时,这个新定义的原型对象不含有constructor属性,所以类的实例也不含有constructor属性。

constructor属性

  • 每一个prototype原型对象都包含惟一一个不可枚举属性constructor,constructor属性的值是一个函数对象。
  • 看前面的代码,咱们是在prototype原型对象上新增了一个方法,而不是重定义一个对象。
  • 因此构造函数的原型中存在预约义好的constructor属性,这意味着对象一般继承的constructor均指代它们的构造函数,因为构造函数是类的‘公共标识’,所以这个constructor属性为对象提供了类
  • 因此当咱们People.prototype = {},这种写法时会破坏原型和原型链。

解决方案

显式给原型添加一个构造函数

function People(name, age) {
    this.name = name;
    this.age = age;
};
People.prototype = {
    constructor: People,
    showName: function () {
        console.log(this.name);
    }
}

// 使用构造函数
var p1 = new People('wqh', 18);
p1.showName();
var p2 = new People('w', 19);
var p3 = new People('q', 20);
console.log(p1, p2, p3);
复制代码

2.class写法

class People {
    constructor (name, age) {
        this.name = name;
        this.age = age;    
    }
    
    showName () {
        console.log(this.name);
    }
};

// 使用构造函数
var p1 = new People('wqh', 18);
p1.showName();
var p2 = new People('w', 19);
var p3 = new People('q', 20);
console.log(p1, p2, p3);
复制代码

注意点

  • 这里的constructor与前面构造函数里的constructor做用不同。构造函数里函数是能够传参数的,而class里没有参数,可是咱们在new是时候又能够传参数,因此class语法里给咱们提供了constructor方法来实现传参。
  • constructor方法是类的默认方法,经过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,若是没有显式定义,一个空的constructor方法会被默认添加。
  • 构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的全部方法都定义在类的prototype属性上面。
class People {
  constructor() {
    // ...
  }
  showName() {
    // ...
  }
}

// 等同于
People.prototype = {
  constructor() {},
  showName() {},
};

// 在类的实例上面调用方法,其实就是调用原型上的方法。
// p2.constructor === People.prototype.constructor // true
复制代码
  • 实例属性除了定义在constructor()方法里面的this上面,也能够定义在类的最顶层。
class People {
    hello = 'hello';
    world = 'world';
    constructor (name, age) {
        this.name = name;
        this.age = age;    
    }
    
    showName () {
        console.log(this.name);
    }
};

// 使用构造函数
var p1 = new People('wqh', 18);
p1.showName();
var p2 = new People('w', 19);
var p3 = new People('q', 20);
console.log(p1, p2, p3);
复制代码

class 更多相关知识请看 es6.ruanyifeng.com/#docs/class

常常据说一句话 ——— ‘js一切皆对象’(排除基本类型)。咱们一开始就说函数是特殊的对象,因此从函数到类其实都是对象,类也是函数,由于类的数据类型就是函数。因此在js世界里,一切皆对象是真实存在的。

因此下次打算梳理 ——— ‘js面向对象编程’。