函数
1. 理解特性
- 一般来说,一个函数是可以通过外部代码调用的一个“子程序”(或在递归的情况下由内部函数调用)。
- 在 JavaScript中,函数是头等(first-class)对象,因为它们可以像任何其他对象一样具有属性和方法,也可以赋值给变量或作为参数传递给其它函数。(它们与其他对象的区别在于函数可以被调用。简而言之,它们是Function对象。)
- 如果一个函数中没有使用return语句,则它默认返回undefined。
- 调用函数时,传递给函数的值被称为函数的实参(值传递),对应位置的函数参数名叫作形参。(如果实参是一个包含原始值(数字,字符串,布尔值)的变量,内部改变对应形参的值,返回后,该实参变量的值也不会改变。如果实参是一个对象引用,则对应形参会和该实参指向同一个对象。假如函数在内部改变了对应形参的值,返回后,实参指向的对象的值也会改变),所以这里经常会有一个深浅拷贝问题
- 每次调用时还有拥有另一个值---本次调用的上下文---这就是this关键字的值
- 当函数嵌套在其它函数中定义,这时它就可以访问它被定义时所处的作用域中的任何变量(这一位着javascript函数构成一个闭包)。
2. 函数定义
2.1. 函数声明 (函数语句)
function name([param[, param[, ... param]]]) {
statements
}
// name 函数名.
// param 传递给函数的参数的名称,一个函数最多可以有255个参数.
// statements 组成函数体的声明语句.
函数声明语句“被提前”到外部脚本或外部函数作用域的顶部,所以可以被在它定义之前出现的代码所调用。
2.2. 函数表达式
var myFunction = function name([param[, param[, ... param]]]) {
statements
}
// name 函数名,可以省略。当省略函数名的时候,该函数就成为了匿名函数。
// param 与 statements的作用和函数声明中一样。
函数表达式不会提升,所以不能在定义之前调用。
2.3. 箭头函数表达式
([param] [, param]) => { statements }
param => expression
// param 参数名称. 零参数需要用()表示. 只有一个参数时不需要括号. (例如 foo => 1)
// statements or expression 多个声明statements需要用大括号括起来,而单个表达式时则不需要。表达式expression也是该函数的隐式返回值。
3.函数属性和方法
因为函数是javascript中的特殊对象,所以它们也可以拥有属性和方法。
3.1. length属性
函数的length属性是只读属性,他代表函数实参的数量。
3.2. prototype属性
- 每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象叫‘原型对象’(prototype object)。
- 每一个函数都包含不同的原型对象。
- 当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性。
3.3. call()方法和apply方法()
了解更多请看这篇文章 juejin.cn/post/684490…
3.4. bind()方法
同上
3.4. toString()方法
方法返回一个字符串,这个字符串和函数声明语句的语法有关,大多数(非全部)的toString()方法的实现都返回函数的完整源码。
构造函数(constructor)
所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。
构造函数就是一个普通的函数
var Animal = function () {
this.name = 'dog';
};
上面代码中,Animal就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写。
构造函数的特点有两个。
- 函数体内部使用了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 的设计是一个简单的基于对象的范式。一个对象就是一系列属性的集合,一个属性包含一个名和一个值。一个属性的值可以是函数,这种情况下属性也被称为方法。除了浏览器里面预定义的那些对象之外,你也可以定义你自己的对象。
对象基础知识请看这三篇文章
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…
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世界里,一切皆对象是真实存在的。