JavaScript Function

聊一下函数...

函数声明方式

具名函数

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

let fun = function fun1() {
  console.log('function1');
};
console.log(fun.name); // fun1
console.log(fun1.name); // Uncaught ReferenceError: fun1 is not defined
fun(); // function1
fun1(); // Uncaught ReferenceError: fun1 is not defined

let obj = {
  fun: function test() {
    console.log(this.name);
  },
  hello: function () {
    console.log('hello');
  },
  name: 'test'
};
console.log(obj.fun.name); // test
console.log(obj.hello.name); // hello
obj.fun(); // test
obj.hello(); // hello
obj.fun.call({ name: 'hxb' }); // hxb
test(); // 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)); // 2

let fun1 = (i, j) => i + j;
console.log(fun1(1, 2)); // 3

let 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(); // test
fun2(); // test
hasNameFun(); // test
fun3(); // test
console.log(fun1.name); // fun1
console.log(fun2.name); // fun1,fun1 和 fun2 指向的是同一个 function。
console.log(hasNameFun.name); // fun1
console.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 是 obj
fun1(); // 打印出的 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 World
hi.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(); //输出 window
fun1(); // 输出 window
obj.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);

// ES6
const 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); // hxb
hxb.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); // test
test.sayName(); // test

function 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); // child
child.sayName(); // child

let person = { name: 'oqm' };
test.sayName.call(person); // oqm
test.sayName.apply(person); // oqm
let newBind = test.sayName.bind(person); // 返回一个函数
newBind(); // oqm
  • call 和 apply 虽然第一个参数都是要改变上下文的对象,但是 call 后面的参数是以参数列表的形式传入,而 apply 则是以数组的形式传入,并且据说在参数少时,call 的性能要大于 apply。
let testArr = [1, 2, 3, 4];
// 求数组中的最值
console.log(Math.max(...testArr)); // 4
console.log(Math.max.call(null, 1, 2, 3, 4)); // 4
console.log(Math.max.call(null, testArr)); // NaN
console.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')); // hxb
console.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 0

var b = fun(0).fun(1).fun(2).fun(3);
// undefined 0 1 2

var 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; // true
Object 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; //true
Function.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); // function
console.log(func()); // 300
// 或者
let test = 200;
let func = function () {
  let test = 100;
  return test + this.test;
};
console.log(typeof func); // function
console.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); // 300
console.log(typeof func); // object
console.log(func.constructor()); // 300

  这好像是定一个函数,但是实际上这是定一个 JavaScript 中的用户自定义复杂对象,不过这里是个匿名类。这个用法和函数本身的使用基本没有任何关系,在大扩号中会构建一个变量作用域,this 指代这个作用域本身

使用方法三
let func = new Function('let test = 100; this.test = 200; return test + this.test;');
console.log(typeof func); // function
console.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); // 2
sayHi(); // 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); // object

let 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); // true
console.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); // this
console.log($newObj3.prototypeVar); // prototype
console.log(Object.values($newObj3)); // ['this']
console.log(Object.getPrototypeOf($newObj3)); // {prototypeVar: 'prototype', constructor: ƒ}
console.log($newObj3.__proto__ == testNew3.prototype); // true

参考来源

转改自思否


DoubleAm
10 声望0 粉丝