JS函数
函数实际上也是对象. 每一个函数对象都是Function类型对的实例, Function跟其他引用类型一样, 也有属性和方法.
因为函数是对象, 所以函数名就是指向函数对象的指针, 而不一定与函数本身紧密绑定.
ECMAScript 没有函数重载, 如果定义了两个重名函数, 后面一个函数会覆盖掉原来的函数.
函数的创建
-
函数声明的方式进行创建
function name(params) { //代码主体}
使用该方式 定义的函数 会进行函数声明提升; 而通过表达式创建的函数不会进行声明提升
JavaScript引擎在任何代码执行之前, 会先读函数声明, 并执行上下为中生成函数定义. 而函数表达式必须等到代码执行到它那一行, 才会在执行上下文中生成函数定义
-
函数表达式的方式进行创建
let func = function (params) { //代码主体}
在使用函数表达式初始化变量时, 也可以给函数一个名称, 但一般不需要,因为没有什么作用, 除非进行函数递归.
-
箭头函数
let fuc = () => { //代码主体}
-
Function构造函数
let func = new Function('params', 'func')//这段代码会被解释两次//第一次是当作普通的代码进行解释//第二次是解释传递给构造函数的字符串
箭头函数
-
箭头函数的形式
如果只有一个参数, 可以不使用括号; 只有在没有参数或者多个参数的情况下, 才需要使用括号.
如果不使用大括号, 那么箭头后面就只能有一行代码, 另外, 省略大括号会隐式返回这行代码的值
-
什么时候使用箭头函数?
准备把一个函数当做一个参数传入到另一个函数中时。(箭头函数非常适合嵌入函数的场景)
-
箭头函数和其他函数声明的区别
箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的
箭头函数不能使用arguments super 和new.target ,也不能用作构造函数, 箭头函数也没有prototype属性
-
箭头函数中的this绑定问题
箭头函数中的this是如何查找的呢?(从箭头函数定义的位置进行查找而不是调用的位置进行查找)
向外层作用域中一层层查找this,直到有this的定义。在对象内部的方法内是存在this的隐形定义的,指向这个对象。而在settimeout中的function定义方法的查找方法是通过call方法,第一个参数绑定的是window对象。
const obj = { bbb() { setTimeout(function(){ setTimeout(function() { console.log(this)//指向window }); setTimeout(() => { console.log(this)//指向window 向外查找时,查找到上层的function函数时,由于其函数通过call方法调用,因而其隐含定义了this指向window }) }) setTimeout(() => { setTimeout(funciton() { console.log(this)//类似function的写法都是指向window }); setTimeout(() => { console.log(this)//指向obj 该this向外层进行查找时,查找到上层的箭头函数,此箭头函数中的this定义指向obj }) }) }}
函数名
函数名是指向函数的指针, 因此它和其他包含对象指针的变量具有相同的行为, 因此, 一个函数可以有多个名称
函数的name属性: 其中包含函数的信息(多数情况下, 这个属性保存的是一个函数标识符, 或者说是一个字符串化的变量名)
如果函数没有名称, 则该值为空字符串, 如果函数是通过Function构造函数创建的, 则会标识成'anonymous'
如果函数是一个获取函数、设置函数,或者使用bind()实例化, 那么标识符前面会加上一个前缀(bound, get set)
函数的参数
ECMAScript 函数不关系传入的参数个数, 也不关心这些参数的数据类型.
默认参数
ES5中实现默认参数的一种常用方式是检测某个参数是否等于undefined, 如果是则意味着没有传这个参数, 那就给它赋一个值
ES6中支持显示定义默认参数, 即在函数的命名参数中带入赋值
给函数传undefined相同于没有传值, 这样可以利用多个独立的默认值
arguments对象的值不反应参数的默认值, 只反映传给函数的参数(即函数调用的时候)
默认参数并不限于原始值或对象类型, 也可以使用调用函数返回的值
函数的默认参数只有在函数被调用时才会求值, 不会在函数定义时求值
箭头函数同样可以使用默认参数, 但只有一个参数时, 括号不能省略
参数初始化遵循'暂时性死区'规则, 后定义的默认参数可以引用先定义的参数, 而前定义的参数不能引用后面定义的
参数也存在自己的作用域中, 他们不能引用函数体的作用域
拓展符(...)
可以用于调用函数时传参, 也可以用于定义函数参数
作用: 将数组元素或集合元素或字符串拆分然后传入
对于函数中的arguments对象来说, 它并不知道扩展操作符的存在, 而是按照调用函数时传入的参数接受每一个值
可以使用扩展操作符把不同长度的独立参数组合为一个数组.
收集参数的前面如果还有命名参数, 则只会收集其余的参数; 如果没有则会得到空数组, 因为收集参数的结果可变, 因此, 只能将其作为最后一个参数.
arguments对象
在使用function关键字定义(非箭头)函数时, 可以在函数内部访问arguments对象, 从而取得传进来的每个参数值
arguments对象是一个类数组对象
arguments的值始终会与对应的命名参数同步, 即通过arguments修改传入参数的值, 实际传入的参数的值也会发生改变, 但这并不意味着, 它们都是访问的同一内存地址, 他们在内存中还是会分开的, 只不过会保持同步而已
(另外, 如果只传入了一个参数, 那么修改arguments[1]的值, 这个值并不会反映到第二个命名参数, 这是因为 arguments对象的长度是根据传入的参数个数, 而非定义函数时给出的命名参数个数确定的)
(严格模式下, 通过修改arguments对象的值不会改变实际传入的命名参数的值, 在函数中尝试重写arguments对象会导致语法错误)
箭头函数中的参数 (由于箭头函数不能使用arguments关键字访问), 因而箭头函数只能通过定义的命名参数访问
另外, 箭头函数没有arguments对象, 但是可以在包装函数中把它提供给箭头函数.
arguments参数类数组对象的属性和方法
-
length属性: 检查传入到函数的参数个数
-
callee属性 严格模式下访问会报错, 指向arguments对象所在函数的指针. 可以用于降低递归函数的紧密耦合
-
caller属性 严格模式下访问会报错, 非严格模式下访问始终是undefined, 为了和函数的属性caller进行区分
this对象
this引用的是把函数当成方法调用的上下文对象
在箭头函数中, this引用的是定义箭头函数的上下文, 而不是调用
-
为什么使用this(逻辑是这样的,如果可以不使用this就能实现我们的需求,为什么还要了解它呢)
this提供了一种更优雅的方式来隐式传递一个对象引用,因此可以将API设计得更加简洁并且易于复用。
-
this的绑定规则
-
new绑定
-
显示绑定(call/apply/bind)
-
隐式绑定
-
默认绑定
箭头函数绑定 寻找上一层作用域中是否指明this
setTimeout绑定(函数回调) 都绑定在window对象上 因为这里面的匿名函数都绑定在widow的方法
-
-
改变this绑定对象的方法
call/apply/bind函数详解
-
call()
call()方法在使用一个指定的this值和若干个指定的参数值(接受的是参数列表)的前提下调用某个函数或方法
var foo = { value: 1}function bar() { console.log(this.value)}bar() //this绑定在window value的值为undefinedbar.call(foo); //1
注意: call改变了this的指向, 指向到foo/ bar函数执行了
call的原生实现思路:
首先是this的绑定, 通过将函数绑定为对象的方法, 然后调用方法, 最后删掉方法来实现 绑定this并执行函数
call的原生实现代码:
Function.prototype.call1 = function (context) { //1.取得调用函数并绑定 context = context || window; context.fn = this; var args = []; for (var i = 1 , len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']') } var result = eval('context.fn('+ args +')') delete context.fn; return result;}
-
apply()
apply的应用和call一样, 只不过其余的参数接受的是数组的形式, 而不是列表
原生实现: 代码如下:
Function.prototype.apply1 = function (context, arr) { let context = Object(context) || window; context.fn = this; let result; if (!arr) { result = context.fn(); } else { var argus = []; for (let i = 0, len = arr.length ; i < len ; i++) { argus.push('arr['+ i +']') } result = eval('context.fn('+ argus +')') } delete context.fn; return result; }
-
bind()
bind()方法会创建一个新函数, 当这个新函数被调用时, bind() 的第一个参数将作为它运行时的this, 之后的一系列参数将会在传递的实参前传入作为它的参数.
功能: 对于普通函数, 绑定this指向/对于构造函数, 保证原函数的原型对象上的属性不能丢失(bind返回的函数作为构造函数的时候, bind时指定的this值会失效, 但传入的参数依然生效)
原生实现: 代码如下:
// 当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面一句 `fbound.prototype = this.prototype;`,已经修改了 fbound.prototype 为 绑定函数的 prototype,此时结果为 true,当结果为 true 的时候,this 指向实例。 Function.prototype.bind1 = function (context) { if (typeof this !== 'function') { throw new Error('Function.prototype.bind - what is trying to be bound is not callable') } let self = this; let args = Array.prototype.slice.call(arguments, 1); let fNOP = function () {}; let fbound = function () { let bindArgs = Array.prototype.slice.call(arguments); return self.apply(this instanceof self ? this : context, args.concat(bindArgs)); } fNOP.prototype = this.prototype; fbound.prototype = new fNOP(); return fbound; }
-
例自定义构造函数内部的属性或方法定义时常使用this。
如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用,则this在非严格模式等于window,在严格模式下等于undefined。如果作为某个对象的方法调用,则this等于这个对象。
**每个函数在调用时,都会自动创建两个特殊变量:this和arguments。**内部函数永远不可能直接访问外部函数的这两个变量。但是可以把this保存到闭包可以访问的另一个变量中。(this和argument都是不能直接在内部函数中访问的。如果想访问包含作用域中的argument对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。)
函数的属性
-
length属性 保存函数定义的命名参数的个数
-
prototype属性 指向构造函数的原型, 保存引用类型所有实例方法的地方 不可以枚举该属性.
-
name属性 其中包含函数的信息(多数情况下, 这个属性保存的是一个函数标识符, 或者说是一个字符串化的变量名)
如果函数没有名称, 则该值为空字符串, 如果函数是通过Function构造函数创建的, 则会标识成'anonymous'
如果函数是一个获取函数、设置函数,或者使用bind()实例化, 那么标识符前面会加上一个前缀(bound, get set)
未赋值给其他变量的匿名函数的name属性是空字符串.
-
caller属性 该属性引用的是调用当前函数的函数或者如果是在全局作用域中调用的则为null. 严格模式下, 不能给caller属性赋值, 否则会导致错误
-
new.target属性 为了辨别函数是普通调用还是通过new操作符作为构造函数进行调用
普通调用时, 该值为undefined
构造函数调用时, 该值指向被调用的构造函数
函数的方法
-
call方法
接受的一个参数为this值(或者要绑定的对象), 其余的参数是传递给函数的(并且需要逐个传递)
//call方法的原生实现 Array.prototype.call2 = function() { //取得要绑定的对象 let obj1 = arguments[0]; //取得调用函数并绑定 let func = this; obj1.fn = func; //取得调用函数的参数 let args = [] for (let i = 1; i < arguments.length; i++) { args.push('arguments['+ i +']'); } //调用函数, 并取得返回的结果 let res = eval('obj1.fn('+ args +')') //删除为该对象显示设置的属性 delete obj1.fn; //返回函数调用结果 return res; }
-
apply方法
和call方法的作用相同, 不同之处在于第二个参数, 它是一个数组, 将要传递给函数的参数, 组成一个数组传入.
//apply方法的原生实现 Array.prototype.apply2 = function() { //取得要绑定的对象 let obj1 = arguments[0]; //取得调用函数并绑定 let func = this; obj1.fn = func; //取得调用函数的参数 let args = []; let arr = arguments[1] for (let i = 0; i < arr.length; i++) { args.push('arr['+ i +']'); } //调用函数, 并取得返回的结果 let res = eval('obj1.fn('+ args +')') //删除为该对象显示设置的属性 delete obj1.fn; //返回函数调用结果 return res; }
-
bind()方法
创建一个新的函数实例, 其this值会被绑定得到传给bind()的对象
// 当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面一句 `fbound.prototype = this.prototype;`,已经修改了 fbound.prototype 为 绑定函数的 prototype,此时结果为 true,当结果为 true 的时候,this 指向实例。 Function.prototype.bind1 = function (context) { if (typeof this !== 'function') { throw new Error('Function.prototype.bind - what is trying to be bound is not callable') } let self = this; let args = Array.prototype.slice.call(arguments, 1); let fNOP = function () {}; let fbound = function () { let bindArgs = Array.prototype.slice.call(arguments); return self.apply(this instanceof self ? this : context, args.concat(bindArgs)); } fNOP.prototype = this.prototype; fbound.prototype = new fNOP(); return fbound; }
在严格模式下, 调用函数时如果没有指定上下文对象, 则this值不会指向window. 除非使用apply()或call()把函数指定给一个对象, 否则this的值会变成undefined.
递归&&尾调用(优化)
递归函数通常的形式是一个函数通过名称来调用自己.
如何解决递归的紧密耦合? (通过arguments对象的callee属性)(严格模式下, 访问该属性会出错, 可以使用命名函数表达式来解决)
//命名函数表达式来解决递归紧密耦合
let factorial = (function f(num) {
if(num <= 1) {
return 1;
} else {
return num * f(num - 1)
}
})
尾调用(优化)
尾调用: 外部函数的返回值是一个内部函数的返回值
function outerFunction() {
return innerFunction()
}
尾调用优化条件:
-
代码在严格模式下执行
-
外部函数的返回值是对尾调函数的调用
-
尾调用函数返回后不需要执行额外的逻辑
-
尾调用函数不是引用外部函数作用域中自由变量的闭包
差异化尾调用和递归尾调用都可以应用优化(引擎不区分尾调用中调用的是函数自身还是其他函数)
闭包(closure)
闭包(closure)指的是那些引用了另一个函数作用域中变量的函数, 通常是在嵌套函数中实现的(作用域链的查找规则)
根据js的词法作用域,若要引用另一个函数体中的变量,则其必须是这个函数的内部函数,这样它就可以访问外层函数体中的变量。另外,函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量,函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域了。根据上面的逻辑,对于外层函数来说,其内部定义的变量是内层函数的全局变量,因此在通过return将内层函数返回并调用时,外层函数的内部变量并不会销毁,会一直存在,直到内层函数不再调用为止,内层函数内定义的变量只会在调用时存在,不调用时则会销毁。
在调用一个函数时, 会为这个函数调用创建一个执行上下文, 并创建一个作用域链. 然后用arguments和其他命名参数来初始化这个函数的活动对象.外部函数的活动对象是内部函数作用域链上的第二个对象(第一个对象是其本身的活动对象). 这个作用域链一直向外串起了所有包含函数的活动对象, 直到全局执行上下文才终止.
函数执行时, 每个执行上下文中都会有一个包含其中变量的对象. 全局上下文中的叫变量对象, 它会在代码执行期间始终存在. 而函数局部上下文中的叫活动对象, 只在函数执行期间存在.
函数定义时, 就会创建作用域链, 预装载到某个对象中, 并保存在内部的[[scope]]中; 在函数执行时, 会创建相应的执行上下文, 然后通过复制函数的[[scope]]来创建其作用域链. 接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端.
闭包函数的this指向问题
函数在调用时, 会自动创建两个特殊变量: this和arguments 来初始化这个函数的活动对象.
内部函数永远不可能直接访问外部函数的 这两个变量, 如果将其保存到闭包可以访问的另一个变量中, 这是可以实现的
闭包函数的调用位置决定了this的指向问题.
立即调用的函数表达式
立即调用的匿名函数又被称为立即调用的函数表达式(IIFEE immediately invoked function expression)
它类似于函数声明, 但由于被包含在括号中, 所以会被解释为函数表达式.
使用IIFE(立即调用的函数表达式)可以模拟块级作用域
(function (params) {
//块级作用域
})();
for循环中,使用let声明的块级作用域,如果要把let声明语句放在 for循环外面, 则会达不到目的(和使用var类似的效果)(因为不会在每次循环中重新创立一个新的变量来保存数据)
私有变量
任何定义在函数或块中的变量都可以认为是私有的, 因为在这个函数或块的外部无法访问其中的变量
私有变量包括函数参数, 局部变量, 以及函数内部定义的其他函数
如何访问私有变量呢?
特权方法(privilege method):
能够访问函数私有变量(及私有函数)的共有方法(本质上来说通过函数闭包来实现)
构造函数实现
通过构建一个构造函数, 在构造函数内部定义私有变量和函数, 然后通过this给实例设定一个方法(函数), 可以访问函数体内的私有变量和函数
function MyObject() {
//定义私有变量和函数
let privateVariable = 10;
function privateFunction() {
return false;
}
//定义特权方法 可以访问上方的私有变量和函数
this.publicMethod = function () {
privateVariable ++;
return privateFunction();
}
}
let obj = new MyObject();
obj.publicMethod()// 通过调用这个函数来实现对私有变量的访问和操作
在构造函数中定义的私有变量对于每个实例来说, 都是独一无二的, 因为每次调用构造函数的时候, 都会重新创建一套变量和方法
问题: 和继承那里提到的构造函数模式存在的问题一样, 就是需要给每一个实例创建一遍新方法.
静态私有变量
特权方法的另一种实现思路: 通过使用私有作用域定义私有变量和函数来实现
这个模式定义的构造函数没有使用函数声明, 使用的是函数表达式
另外, 声明MyObject并没有使用任何关键字, 因为不使用关键字声明的变量会创建在全局作用域中, 所以该变量变为了全局变量, 在函数外能够别访问.
这个模式与前一个模式的主要区别就是, 私有变量和私有函数是由实例共享的(通过原型链实现的)(通过定义在构造函数原型上的方法来访问私有变量和私有方法, 而这个原型方法是所有实例所共享的, 因而私有变量和私有方法也被所有实例共享)
// 静态私有变量实现特权方法
(function () {
//定义私有变量和私有方法
let privateVariable = 10;
function privateFunction() {
return false;
}
//构造函数
MyObject = function () {};
//公有和特权方法
MyObject.prototype.publicMethod = function () {
privateVariable ++;
return privateFunction();
}
})();
let obj1 = new MyObject();
obj1.publicMethod();
模块模式
在一个单例对象上实现相同的隔离和封装
单例对象(singleton) 就是只有一个实例的对象
通过在JavaScript中, 单例对象的创建通过对象字面量来进行创建.
模块模式是在单例对象基础上加以扩展, 使其通过作用域链来关联私有变量和特权方法
// 模块模式
let application = function () {
//私有变量和私有方法的定义
let privateVariable = 10;
function privateFunction() {
return false;
}
//特权或公有方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable ++;
return privateFunction();
}
}
}()
模块模式使用了匿名函数返回一个对象, 则该application直接接受返回的对象(因为这个匿名函数也是立即执行的).
在这个匿名函数的内部, 首先定义私有变量和私有函数, 之后创建一个要通过匿名函数返回的对象字面量, 这个对象自卖能量中只包含可以公开访问的属性和方法.
本质上, 对象字面量定义了单例对象的公共接口, 如果单例对象需要进行某种初始化, 并且需要访问私有变量时, 可以采用这个模式
模块增强模式
在前面的模块模式的基础上, 在返回单例对象之前, 对该对象进行了增强
这种模式适合单例对象需要是某一特定类型的实例, 但又必须给它添加额外属性或方法的场景
// 模块增强模式
let application = function () {
//私有变量和私有方法的定义
let privateVariable = 10;
function privateFunction() {
return false;
}
// 创建对象(这里可以是任何构造函数, 包括原生或非原生的)
let obj = new Object();
//特权或公有方法和属性
obj.publicProperty = true;
obj.publicMethod = function() {
privateVariable ++;
return privateFunction();
}
return obj;
}()
私有变量实现的几种方式
通过类实现(ES6的约定)
//私有变量的实现
//1.约定(ES6实现约定) 通过类来实现
class Example {
constructor() {
this._private = 'private';
}
getName() {
return this._private;
}
}
var ex = new Example();
console.log(ex.getName());//private
console.log(ex._private); //private
优点: 写法简单 调试方便 兼容性好
缺点: 外部可以访问和修改 语言没有配合的机制, 如for-in语句会将所有属性枚举出来 命名冲突
通过闭包来实现
-
方式一: 与上面的构造函数实现特权方法类似
//实现一 (与红宝书的构造函数模式相同) class Example { constructor() { var _private = ''; _private = 'private'; this.getName = function () { return _private; } } } var ex = new Example(); console.log(ex.getName()); //private console.log(ex._private); // undefined
优点: 无命名冲突 外部无法访问和修改
缺点: constructor的逻辑变得复杂. 构造函数应该只做对象初始化的事情, 现在为了实现私有变量, 必须包含部分方法的实现, 代码上逻辑略有不清晰
方法存在于实例, 而非原型上, 子类也无法使用super调用
构造增加了一点点开销
-
方式二: 与静态私有变量的实现类似
let Example = (function () { //定义私有变量 var _private = ''; class Example { constructor() { _private = 'private'; } getName() { return _private; } } return Example; })(); var ex = new Example(); console.log(ex.getName()); //private console.log(ex._private); //undefined
优点: 无命名冲突 外部无法访问和修改
缺点: 写法有一点复杂 构建增加一点点开销
通过Symbol实现
实现过程和静态私有变量的实现过程类似
const Example = (function () {
var _private = Symbol('private');
class Example {
constructor() {
this[_private] = 'private';
}
getName() {
return this[_private];
}
}
return Example;
})()
var ex = new Example();
console.log(ex.getName()); //private
console.log(ex._private); //undefined
优点: 无命名冲突 外部无法访问和修改 无性能损失
缺点: 写法稍微复杂 兼容性也还好
通过WeakMap实现
-
实现一
//实现一 const _private = new WeakMap() class Example { constructor() { _private.set(this, 'private'); } getName() { return _private.get(this); } } var ex = new Example(); console.log(ex.getName()); //private console.log(ex.name); //undefined
-
实现二(实现一的不同写法)
//也可以这样写 const Example = (function () { var _private = new WeakMap(); class Example { constructor() { _private.set(this, 'private') } getName() { return _private.get(this); } } return Example; })(); var ex = new Example(); console.log(ex.getName()); //private console.log(ex.name); //undefined
优点: 无命名冲突 外部无法访问和修改
缺点: 写法比较麻烦 兼容性有点问题 有一定性能代价