重学JavaScript【函数的进阶】

简介: 本篇主要收一下函数的内部对象,只有更好的了解函数的内部结构,才可以更好的封装方法。

本篇主要收一下函数的内部对象,只有更好的了解函数的内部结构,才可以更好的封装方法。


arguments


arguments 表示一个函数传入的参数,不管有没有传参,arguments 都存在。

function fn(){
  console.log(arguments);
}
fn(1, 2, 3);
// Arguments [1, 2, 3, callee: fn(), Symbol(Symbol.iterator)]
fn();
// undefined


可以看出有参数传入的话,arguments就是一个类数组,保存了传入的值,如果没有参数,就是undefined。既然arguments可以获得参数,那当然可以使用arguments操作参数了,如果赋值了arguments再传参呢?

function fn(){
  arguments[1] = 5;
  console.log(arguments);
}
fn(1);
// Arguments [1, 5, callee: fn(), Symbol(Symbol.iterator)]


注意:如果只传了一个参数,然后把 arguments[1] 设置为某个值,那么这个值不会反映到第二个命名参数,因为arguments的长度是根据传入的参数个数,而不是定义函数时给出的命名参数个数。

如果使用箭头函数,是无法获取 arguments 的。

arguments 有一个 callee 属性,它指向 arguments 所在的函数,下面是一个通过递归阶乘的函数:

function factorial(num){
  if(num <= 1){
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}


内部调用了 factorial 自己,相当于上面的代码写了两次 factorial,通过 arguments.callee 就可以只写一次,且易读:

function factorial(num){
  if(num <= 1){
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}


好处是什么呢?现在还体现不出来,再看个例子:

let factorial1 = factorial;
factorial = function(){
  return 0;
};
console.log(factorial1(5)); // 120
console.log(factorial(5)); // 0


相当于原来 factorial 的指针又指向了一个新地址:factorial1,然后重写了 factorial。这样如果不使用 arguments.callee 的话,factorial 内部调用的其实不是自己,而是 factorial,使用了 arguments.callee 之后,调用的就是自己了。

callee 英文是被叫者的意思,也可以理解为被叫者就是所在的函数。


caller


caller 是呼叫者的意思,指向函数被调用者:

function fn(){
  gn();
}
funciton gn(){
  console.log(gn.caller);
}
fn();


上面的代码会打印fn函数本身,因为 fn 调用了 gn,所以 gn 的 caller 就指向了 fn,而 gn 其实也是arguments.callee,所以可以这样写:

funciton gn(){
  console.log(arguments.callee.caller);
}


new.target


ECMAScript6定义了 new.target 属性,用于检测函数是否是用 new关键字 调用的,如果不是就返回undefined,如果是就引用被调用的构造函数:

function fn(){
  if(!new.target){
    console.log("没使用new")
  } else {
    console.log("使用了new")
  }
}
new fn(); // 使用了new
fn(); // 没使用new


call


call 可以改变函数体内 this 的指向:

function sum(a, b){
  return a + b;
}
function fn(a, b){
  return sum.callthis, a, b);
}
fn(1, 2); // 3


上面的例子中,fn 调用了 sum,将 fn 内部的 this 传入了 sum,也可以说 this 指向了 sum,同时传入了 arguments。

下面我们实现一个 call:

首先我们分析 call 干了什么事情,改变了this的指向,并且执行了原来的函数,假如是这样调用的:

var obj = {
  name: "abc"
}
function getName(){
  console.log(this.name);
}
getName.call(obj);


那么是不是可以理解为:getName是在obj上被调用了!

var obj = {
  name: "abc",
  getName: function(){
    console.log(this.name)
  }
}
obj.getName();


这样不就和上面的逻辑一样了么,但是这样就给obj无缘无故添加了一个方法,我们把这个方法删掉不就更逼真了么:

var obj = {
  name: "abc",
  getName: function(){
    console.log(this.name)
  }
}
obj.getName();
delete obj.getName;


到这里,我们从头分析一下走了哪几步:

  1. 将函数设置成对象的属性
  2. 执行这个函数
  3. delete 删除这个函数

那么:

Function.prototype.myCall = function(context){
  // this就是call的函数
  context.fn = this; // obj.fn = getName
  context.fn(); // obj.fn()
  delete context.fn; // delete obj.fn
}


一个简单版本的call就重写好了,接下来增加参数,原来的call是这样使用的。

var obj = {
  name: "abc",
}
function getName(first, last){
  console.log(first);
  console.log(last);
  console.log(this.name)
}
getName.call(obj, "a", "bc"); // "a", "bc", "abc"


传入的参数不确定!可以使用前面提到的 arguments!取出第二个到最后一个参数,然后放到一个数组里!

// 上面的那个例子里 arguments是
// arguments = {
//    0: obj,
//    1: "a",
//    2: "b",
//    length: 3
// }
var args = [];
for(var i = 1; i < arguments.length; i++){
  args.push('arguments[' + i + ']');
}
// [obj, "a", "b"]


然后我们要把args放到代码里:

Function.prototype.myCall = function(context){
  // this就是call的函数
  context.fn = this; // obj.fn = getName
  var args = [];
  for(var i = 1; i < arguments.length; i++){
    args.push('arguments[' + i + ']');
  }
  eval('context.fn(' + args + ')');
  delete context.fn; // delete obj.fn
}


到这里要注意两个问题,如果this没有呢?并且函数是可以有返回值的!直接上代码

Function.prototype.myCall = function(context){
  // this 如果没有的话,默认会指向window,
  // this可以传基本类型数据,这种情况的话原生的call处理方式
  // 是将参数用Object()转换一下
  var context = context ? Object(context) : window;
  // this就是call的函数
  context.fn = this; // obj.fn = getName
  var args = [];
  for(var i = 1; i < arguments.length; i++){
    args.push('arguments[' + i + ']');
  }
  var result = eval('context.fn(' + args + ')');
  delete context.fn; // delete obj.fn
  return result;
}


apply


apply 也可以改变函数体内 this 的指向,和 call 用法一样,主不过第二个参数是数组或类数组:

function sum(a, b){
  return a + b;
}
function fn(){
  return sum.apply(this, arguments);
}
fn(1, 2); // 3


下面我们实现一个apply:

Function.prototype.myApply = function (context, arr) {
  var context = Object(context) || window;
  context.fn = this;
  var result;
  if (!arr) {
    result = context.fn();
  }
  else {
    var args = [];
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push('arr[' + i + ']');
    }
    result = eval('context.fn(' + args + ')')
  }
  delete context.fn
  return result;
}


和call的区别就是一个是用arguments,一个使用传入的arr数组。


bind


bind 会创建一个新的函数,在bind被调用的时候,这个新函数的 this 就被指定为 bind 的第一个参数,其余参数将作为新函数的参数传入,供调用时使用。

也就是说,bind方法会创建一个新的函数实例,this也会绑定到这个函数实例上:

var obj = {
  name: "abc"
}
function fn(){
  console.log(this.name);
}
var a = fn.bind(obj);
a(); // abc


上面例子将 fn 的 this 指向了 obj。

我们利用apply,来模拟一下:

Function.prorotype.myBind = function(context){
  var _this = this;
  return function(){
    _this.apply(context);
  }
}


接着优化,优化参数的传递:

Function.prorotype.myBind = function(context){
  var _this = this;
  // 获取第二个到最后一个参数
  var args = Array.prototype.slice.call(arguments, 1);
  return function(){
    //此时的arguments是bind返回的函数的参数
    var bindArgs = Array.prototype.slice.call(arguments);
    _this.apply(context, args.concat(bindArgs));
  }
}


到这里,算是一半了,注意:

一个绑定函数也能使用new操作符创建对象,这种行为就像把原函数当成了构造器。

提供的this值被忽略,同时调用的参数被提供给模拟参数。

也就是说 bind 返回的函数作为构造函数的时候,bind 指定的 this 会失效,但是传入的参数仍然生效。

Function.prorotype.myBind = function(context){
  var _this = this;
  // 获取第二个到最后一个参数
  var args = Array.prototype.slice.call(arguments, 1);
  var fnBound = funciton(){
    var bindArgs = Array.prototype.slice.call(arguments);
    // 当作为构造函数时,this指向实例
    // 当作为普通函数时,this指向绑定的函数context
    // isCreateOrCustom 是true的话,this指向实例
    var isCreateOrCustom = this instanceof _this ? this : context;
    _this.apply(isCreateOrCustom,  args.concat(bindArgs));
  }
  // 修改函数的 prototype 为绑定函数的 prototype
  // 实例就可以继承函数的原型中的值
  fnBound.prototype = this.prototype;
  return fnBound
}


如果此时直接修改 fBound.prototype 了,this.prototype也会改变,所以可以用一个空函数:

Function.prorotype.myBind = function(context){
  var _this = this;
  // 获取第二个到最后一个参数
  var args = Array.prototype.slice.call(arguments, 1);
  var fNOP = function(){}
  var fnBound = funciton(){
    var bindArgs = Array.prototype.slice.call(arguments);
    // 当作为构造函数时,this指向实例
    // 当作为普通函数时,this指向绑定的函数context
    // isCreateOrCustom 是true的话,this指向实例
    var isCreateOrCustom = this instanceof _this ? this : context;
    _this.apply(isCreateOrCustom,  args.concat(bindArgs));
  }
  // 修改函数的 prototype 为绑定函数的 prototype
  // 实例就可以继承函数的原型中的值
  fNOP.prototype = this.prototype;
  fnBound.prototype = new fNOP();
  return fnBound;
}


上面的两句:

fNOP.prototype = this.prototype;
 fnBound.prototype = new fNOP();


可以修改为:

fbound.prototype = Object.create(this.prototype);


因为Object.create内部相当于:

Object.create = function (o) {
  function f(){}
  f.prototype = 0;
  return new f;
}


目录
相关文章
|
17天前
|
JavaScript 前端开发 Java
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
本文介绍了JavaScript中常用的函数和方法,包括通用函数、Global对象函数以及数组相关函数。详细列出了每个函数的参数、返回值及使用说明,并提供了示例代码。文章强调了函数的学习应结合源码和实践,适合JavaScript初学者和进阶开发者参考。
31 2
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
|
16天前
|
前端开发 JavaScript 开发者
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。
|
30天前
|
JavaScript 前端开发
JavaScript 函数语法
JavaScript 函数是使用 `function` 关键词定义的代码块,可在调用时执行特定任务。函数可以无参或带参,参数用于传递值并在函数内部使用。函数调用可在事件触发时进行,如用户点击按钮。JavaScript 对大小写敏感,函数名和关键词必须严格匹配。示例中展示了如何通过不同参数调用函数以生成不同的输出。
|
1月前
|
存储 JavaScript 前端开发
JS函数提升 变量提升
【10月更文挑战第6天】函数提升和变量提升是 JavaScript 语言的重要特性,但它们也可能带来一些困惑和潜在的问题。通过深入理解和掌握它们的原理和表现,开发者可以更好地编写和维护 JavaScript 代码,避免因不了解这些机制而导致的错误和不一致。同时,不断提高对执行上下文等相关概念的认识,将有助于提升对 JavaScript 语言的整体理解和运用能力。
|
1月前
|
JavaScript 前端开发
js教程——函数
js教程——函数
36 4
|
1月前
|
存储 JavaScript 前端开发
js中函数、方法、对象的区别
js中函数、方法、对象的区别
16 2
|
1月前
|
JavaScript 前端开发 Java
【javaScript数组,函数】的基础知识点
【javaScript数组,函数】的基础知识点
24 5
|
1月前
|
JavaScript 前端开发
Node.js 函数
10月更文挑战第5天
23 3
|
1月前
|
前端开发 JavaScript
探索JavaScript函数基础
探索JavaScript函数基础
19 3
|
1月前
|
存储 JavaScript 前端开发
JavaScript数据类型全解:编写通用函数,精准判断各种数据类型
JavaScript数据类型全解:编写通用函数,精准判断各种数据类型
19 0