JavaScript Function

聊一下函数...

函数申明形式

具名函数

  function 前面有函数名字的,不是间接跟括号的的就是具名函数。如果把一个具名函数赋值给一个变量,那么这个具名函数的作用域就不是 window 了(严格模式下 this 指向 undefined),且函数调用的名字也为变量名,对象中定义同理,不论有没有函数名称,最终调用的函数名均为此变量或属性名称。
  具名函数的 this 指向是当咱们调用函数的时候确定的,调用形式的不同决定了 this 不同的指向,个别指向咱们的调用者。

let fun = function fun1() {  console.log('function1');};console.log(fun.name); // fun1console.log(fun1.name); // Uncaught ReferenceError: fun1 is not definedfun(); // function1fun1(); // Uncaught ReferenceError: fun1 is not definedlet obj = {  fun: function test() {    console.log(this.name);  },  hello: function () {    console.log('hello');  },  name: 'test'};console.log(obj.fun.name); // testconsole.log(obj.hello.name); // helloobj.fun(); // testobj.hello(); // helloobj.fun.call({ name: 'hxb' }); // hxbtest(); // Uncaught ReferenceError: test is not defined

箭头函数

箭头函数是 ES6 知识点,具备以下几个特点。
  • 如果只有一个参数,能够省略小括号。
  • 如果有至多有两个参数,必须加小括号。
  • 如果函数体只有一句话能够省略花括号,并且这一句作为返回值 return。
  • 如果函数体至多有两句必须加上花括号。
  • 箭头函数没有本人的 this,它的 this 继承于运行时的外层代码库的第一个 this,由上下文决定,所以也不能用call、apply、bind 去扭转 this 的指向
  • 箭头函数不能应用 arguments、super 和 new.target,也不能用作构造函数。
  • 箭头函数没有 prototype 属性。
let fun = e => e + 1;console.log(fun(1)); // 2let fun1 = (i, j) => i + j;console.log(fun1(1, 2)); // 3let fun2 = (i, j) => {  i += 1;  j += 2;  return i + j;};console.log(fun2(1, 2)); // 6

匿名函数

  • function 前面间接跟括号,两头没有函数名的就是匿名函数。因为匿名函数的执行环境具备全局性,所以匿名函数的 this 指向个别都是 window。
let fun1 = function () {  console.log('test');};function hasNameFun() {  console.log('test');}let fun2 = fun1;let fun3 = hasNameFun;fun1(); // testfun2(); // testhasNameFun(); // testfun3(); // testconsole.log(fun1.name); // fun1console.log(fun2.name); // fun1,fun1 和 fun2 指向的是同一个 function。console.log(hasNameFun.name); // fun1console.log(fun3.name); // fun1,fun1 和 fun2 指向的是同一个 function。var test = 'window';function testFun() {  let test = 'has_name';  console.log('具名函数', this.test, test);  (function () {    let test = 'no_name';    console.log('匿名函数', this.test, test);  })();}testFun.call({ test: '扭转指向的test' });// 具名函数 扭转指向的test has_name// 匿名函数 window no_name

this、arguments、new.target、caller

函数调用

在 ES5 中,函数有四种调用形式。
fun(arg1, arg2); // 等价于 fun.call(undefined, arg1, arg2); || fun.apply(context, [arg1, arg2]);obj.fun(arg1, arg2);fun.call(context, arg1, arg2);fun.apply(context, [arg1, arg2]);// 第三和第四种才是失常的 js 函数调用形式,其余两种就是语法糖。
如果你传的 context 是 null 或者 undefined,那么 window 对象就是默认的 context (严格模式下默认 context 是 undefined)。

this

this 就是一个指针,指向调用函数的对象,并不是指向本身。各个函数的个别指向后面也都有介绍,上面咱们来看看一些小栗子。
let obj = {  fun: function () {    console.log(this);  }};let fun1 = obj.fun;obj.fun(); // 打印出的 this 是 objfun1(); // 打印出的 this 是 window
  • 在执行函数的时候,this是暗藏的一个参数,且必须是一个对象,如果不是,js 是主动把它转为对象。
function fun() {  console.log(this);  console.log(arguments);}fun.call(1, 2, 3);// Number {1}// Arguments(2) [2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
  • this 的绑定有以下几种形式
  1. 默认绑定
  2. 隐式绑定
  3. 显式绑定(硬绑定)
  4. new 绑定
  5. 箭头函数的 this
var name = 'World';var obj = {  name: 'Obj',  sayHi: sayHi};function sayHi() {  console.log('Hello', this.name);}// 默认绑定sayHi(); // Hello World// 隐式绑定obj.sayHi(); // Hello Obj// 显式绑定var hi = obj.sayHi;hi(); // Hello Worldhi.call({ name: 'DoubleAm' }); // Hello DoubleAm// Ps: 如果咱们将 null 或者是 undefined 作为 this 的绑定对象传入 call、apply 或者是 bind,这些值在调用时会被疏忽,理论利用的是默认绑定规定。(严格模式下则不会疏忽)hi.call(null); // Hello World// new 绑定function sayHi(name) {  this.name = name;}var newHi = new sayHi('newName');console.log('Hello', newHi.name); // Hello newName// 箭头函数,咱们能够看作为找外层一般函数的第一个 this。var obj = {  hi: function () {    console.log(this);    return () => {      console.log(this);    };  },  sayHi: function () {    return function () {      console.log(this);      return () => {        console.log(this);      };    };  },  say: () => {    console.log(this);  }};let hi = obj.hi(); // 输入 obj 对象hi(); // 输入 obj 对象let sayHi = obj.sayHi();let fun1 = sayHi(); //输入 windowfun1(); // 输入 windowobj.say(); // 输入 window
参考 this 绑定

arguments

  arguments 是传入的参数,它是伪数组它相似于 Array,但除了 length 属性和索引元素之外没有任何 Array 属性。
  call 和 apply、bind 外面除了第一个参数之外的都是 arguments,如果 arguments 的个数少倡议应用call,参数少时性能更高,应用 apply 也能够,如果不确定就应用 apply。

function test() {  console.log('传入参数', [...arguments]);}test(1, 2, 3); // 传入参数 (3) [1, 2, 3]test(1, 2, 3, 4, 5); // 传入参数 (5) [1, 2, 3, 4, 5]// 把 arguments 转为真正的数组let args = Array.prototype.slice.call(arguments);let args = [].slice.call(arguments);// ES6const args = Array.from(arguments);const args = [...arguments];

new.target

  ECMAScript 中的函数始终能够作为构造函数实例化一个新对象,也能够作为一般函数被调用。
  ES6 新增了检测函数是否应用 new 关键字调用的 new.target 属性。如果函数是失常调用的,则 new.target 的值是 undefined;如果是应用 new 关键字调用的,则 new.target 将援用被调用的构造函数。

function King() {  if (!new.target) {    throw 'King must be instantiated using "new"';  }  console.log('King instantiated using "new"');}new King(); // King instantiated using "new"King(); // Error: King must be instantiated using "new"
这里能够做一些延申,还有没有其余方法来判断函数是否通过 new 来调用的呢?
  • 应用 instanceof 来判断。instanceof 运算符用于检测构造函数的 prototype 属性是否呈现在某个实例对象的原型链上。
function Person() {  // `this instanceof Person` 也可改写为 `this instanceof arguments.callee`  if (this instanceof Person) {    console.log('通过 new 创立');    return this;  } else {    console.log('函数调用');  }}const p = new Person(); // 通过 new 创立Person(); // 函数调用

caller

这个属性援用的是调用以后函数的函数,或者如果是在全局作用域中调用的则为 null。
function outer() {  inner();}function inner() {  console.log(inner.caller);  // 或者 console.log(arguments.callee.caller);}outer(); // function outer() { inner(); }inner(); // null

call、apply、bind

尽管 call、apply、bind 都用于扭转 this 指向,然而还是有区别的。
  • 应用 call/apply 扭转 this 指向后,函数立刻执行,而 bind 则是返回新函数
function Father(name) {  this.name = name;  console.log('初始化');}Father.prototype.sayName = function () {  console.log(this.name);};let hxb = new Father('hxb');console.log(hxb);console.log(hxb.name); // hxbhxb.sayName(); // hxb/** * 实现 new */function _new(fun, ...arg) {  // let obj = {};  // obj.__proto__ = fun.prototype;  let obj = Object.create(fun.prototype);  fun.call(obj, ...arg);  return obj;}let test = _new(Father, 'test');console.log(test);console.log(test.name); // testtest.sayName(); // testfunction Child(name) {  this.__proto__ = Father.prototype; // 继承原型上的办法  console.log(Child.prototype);  Father.call(this, name); // 应用 call 实现继承}let child = new Child('child');console.log(child);console.log(child.name); // childchild.sayName(); // childlet person = { name: 'oqm' };test.sayName.call(person); // oqmtest.sayName.apply(person); // oqmlet newBind = test.sayName.bind(person); // 返回一个函数newBind(); // oqm
  • call 和 apply 尽管第一个参数都是要扭转上下文的对象,然而 call 前面的参数是以参数列表的模式传入,而 apply 则是以数组的模式传入,并且据说在参数少时,call 的性能要大于 apply。
let testArr = [1, 2, 3, 4];// 求数组中的最值console.log(Math.max(...testArr)); // 4console.log(Math.max.call(null, 1, 2, 3, 4)); // 4console.log(Math.max.call(null, testArr)); // NaNconsole.log(Math.max.apply(null, testArr)); //  4,间接能够用 testArr 传递进去。

闭包

闭包指的是那些援用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。像 Vue 中的 data、setTimeout、匿名函数、编写组件时外部数据处理等场景应用较多。
function foo() {  let a = 1;  let b = 2;  function bar() {    return a + b;  }  return bar;}
上述代码中,因为 foo 函数外部的 bar 函数应用了 foo 函数外部的变量,并且 bar 函数 return 把变量 return 了进来,这样闭包就产生了,这使得咱们能够在内部操作并拿到这些变量。
const bar = foo();bar(); // 3

  foo 函数在调用的时候创立了一个执行上下文,能够在此上下文中应用 a,b 变量,实践上说,在 foo 调用完结,函数外部的变量会 js 引擎的垃圾回收机制通过特定的标记回收。
  然而在这里,因为闭包的产生,a,b 变量并不会被回收,这就导致咱们在全局上下文(或其余执行上下文)中能够拜访到函数外部的变量。

无论何时申明新函数并将其赋值给变量,都要存储函数定义和闭包,闭包蕴含在函数创立时作用域中的所有变量,相似于背包,函数定义附带一个小背包,它的包中存储了函数定义时作用域中的所有变量。
  • 写前端组件时,也能够用于爱护咱们的组件外部数据,且避免垃圾回收。
function Widget() {  let data = {};  return {    get: function (key) {      return data[key];    },    set: function (key, value) {      data[key] = value;      return data[key];    }  };}let test = new Widget();test.set('name', 'hxb');console.log(test.get('name')); // hxbconsole.log(data['name']); // Uncaught ReferenceError: data is not defined
  • 以此引出一个经典面试题
for (var i = 1; i <= 5; i++) {  setTimeout(function timer() {    console.log(i);  }, i * 1000);}// 怎么能够使得上述代码的输入变为 1,2,3,4,5?// 咱们能够把 var 换成 let 实现for (let i = 1; i <= 5; i++) {  setTimeout(function timer() {    console.log(i);  }, i * 1000);}// 也能够应用闭包来实现for (var i = 1; i <= 5; i++) {  (function (i) {    setTimeout(function timer() {      console.log(i);    }, i * 1000);  })(i);}

  依据下面的说法,将闭包看成一个背包,背包中蕴含定义时的变量,每次循环时,将 i 值保留在一个闭包中,当 setTimeout 中定义的操作执行时,则拜访对应闭包保留的 i 值,即可解决。

  • one more thing...
function fun(n, o) {  console.log(o);  return {    fun: function (m) {      return fun(m, n);    }  };}var a = fun(0);a.fun(1);a.fun(2);a.fun(3);// undefined 0 0 0var b = fun(0).fun(1).fun(2).fun(3);// undefined 0 1 2var c = fun(0).fun(1);c.fun(2);c.fun(3);// undefined 0 1 1

立刻调用的函数表达式(IIFE)

如下就是立刻调用函数表达式
(function () {  // 块级作用域})();
  • 应用 IIFE 能够模仿块级作用域,即在一个函数表达式外部申明变量,而后立刻调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。
/* IIFE */(function () {  for (var i = 0; i < count; i++) {    console.log(i);  }})();console.log(i); // 抛出谬误/* ES6的块级作用域 */// 内嵌块级作用域{  let i;  for (i = 0; i < count; i++) {    console.log(i);  }}console.log(i); // 抛出谬误// 循环的块级作用域for (let i = 0; i < count; i++) {  console.log(i);}console.log(i); // 抛出谬误
IIFE 的另一个作用就是上文中的解决 setTimeout 的输入问题。

多说几句

对于 instanceof

Function instanceof Object; // trueObject instanceof Function; // true
  • 为什么会这样呢?借用大佬的一张图。

那么由此就能够失去
// 结构器 Function 的结构器是它本身Function.constructor === Function; // true// 结构器 Object 的结构器是 Function(由此可知所有结构器的 constructor 都指向 Function)Object.constructor === Function; // true// 结构器 Function 的 __proto__ 是一个非凡的匿名函数 function() {}console.log(Function.__proto__); //function() {}// 这个非凡的匿名函数的 __proto__ 指向 Object 的 prototype。Function.__proto__.__proto__ === Object.prototype; // true// Object 的 __proto__ 指向 Function 的 prototype,也就是下面中所述的非凡匿名函数。Object.__proto__ === Function.prototype; //trueFunction.prototype === Function.__proto__; //true
  • 论断
  1. 所有的结构器的 constructor 都指向 Function
  2. Functionprototype 指向一个非凡匿名函数,而这个非凡匿名函数_proto_ 指向Object.prototype
  • 顺便发一下原型链图

对于简单对象与 new function/new Function

函数的应用与简单对象

  函数是 JavaScript 中很重要的一个语言元素,并且提供了一个 function 关键字和内置对象 Function,上面是其可能的用法和它们之间的关系。

应用办法一
let func = function () {  let test = 100;  this.test = 200;  return test + this.test;};console.log(typeof func); // functionconsole.log(func()); // 300// 或者let test = 200;let func = function () {  let test = 100;  return test + this.test;};console.log(typeof func); // functionconsole.log(func()); // 300

  最一般的 function 应用形式,定一个 JavaScript 函数。在大扩号内的变量作用域中,this 指代 func 的所有者,即window对象。

应用办法二
let test = 300;let func = new (function () {  let test = 100;  this.test = 200;  return test + this.test;})();console.log(test); // 300console.log(typeof func); // objectconsole.log(func.constructor()); // 300

  这如同是定一个函数,然而实际上这是定一个 JavaScript 中的用户自定义简单对象,不过这里是个匿名类。这个用法和函数自身的应用根本没有任何关系,在大扩号中会构建一个变量作用域,this 指代这个作用域自身

应用办法三
let func = new Function('let test = 100; this.test = 200; return test + this.test;');console.log(typeof func); // functionconsole.log(func.constructor()); // 300

  应用零碎内置函数对象来构建一个函数,这和办法一中的第一种形式在成果和初始化优先级上都完全相同,就是函数体以字符串模式给出。

new Function

这个非凡的new Function外表看起来很奇怪,但在实践中显得十分有用。

  创立一个函数对象的语法是 let func = new Function([arg1, arg2, ...argN], functionBody); 其中,该函数对象的 N 个参数放在函数主体参数 functionBody 的后面,即函数主体参数必须放在参数列表的最初,也能够无参数 new Function(functionBody)

let sum = new Function('a', 'b', 'return a + b');let sayHi = new Function('console.log("Hello")');sum(1, 1); // 2sayHi(); // Hello

  设想一下,咱们必须从字符串创立一个函数。在编写脚本时不晓得该函数的代码(这就是咱们不应用惯例函数的起因),但在执行过程中将会晓得,因为咱们可能会从服务器或其余起源收到它,这时就能够应用此语法创立函数。

参考 new Function

对于函数对象时的 this 与 prototype 区别

  • 构造函数中用 this 和 prototype 定义属性或函数办法的区别

  this 定义的形式,实例化之后是让每一个实例化对象都有一份属于本人的在构造函数中的对象或者函数办法,而 prototype 定义的形式,实例化之后每个实例化对象独特领有一份构造函数中的对象或者函数办法。

/* this */function Obj() {  this.a = []; // 实例变量  this.fn = function () {    // 实例办法  };}let newObj1 = new Obj();newObj1.a.push(1);newObj1.fn = {};console.log(newObj1.a); // [1]console.log(typeof newObj1.fn); // objectlet newObj2 = new Obj();console.log(newObj2.a); // []console.log(typeof newObj2.fn); // function/* ---------- 分割线 ---------- *//* prototype */function Person(name) {  Person.prototype.share = [];}let person1 = new Person();let person2 = new Person();person1.share.push(1);person2.share.push(2);console.log(person1.share); // [1,2]console.log(person2.share); // [1,2]

  最初,一般而言,用 this 来定义构造函数的属性较多,用 prototype 定义构造函数的办法较多,因为属性较于办法来说应用频率更高。你想一想如果每次实例化对象都要执行定义的办法,那对于内存来说就是一种节约。

  • 其余
$newObj1 = new (function testNew1() {  testNew1.prototype.prototypeVar = 'prototype';  this.thisVar = 'this';})();console.log(Object.values($newObj1)); // ['this']console.log(Object.getPrototypeOf($newObj1)); // // {prototypeVar: 'prototype', constructor: ƒ}/* ---------- 分割线 ---------- */function testNew2() {  testNew2.prototype.prototypeVar = 'prototype';  this.thisVar = 'this';}$newObj2 = new testNew2();console.log($newObj2.__proto__ == testNew2.prototype); // trueconsole.log(Object.values($newObj2)); // ['this']console.log(Object.getPrototypeOf($newObj2)); // {prototypeVar: 'prototype', constructor: ƒ}/* ---------- 分割线 ---------- */let testNew3 = new (function () {  let _selfVal = 'prototype';  function testNew3() {    testNew3.prototype.prototypeVar = _selfVal;    this.thisVar = 'this';  }  return testNew3;})();$newObj3 = new testNew3();console.log($newObj3.thisVar); // thisconsole.log($newObj3.prototypeVar); // prototypeconsole.log(Object.values($newObj3)); // ['this']console.log(Object.getPrototypeOf($newObj3)); // {prototypeVar: 'prototype', constructor: ƒ}console.log($newObj3.__proto__ == testNew3.prototype); // true

参考起源

转改自思否