JS学习笔记整理四 apply、call和bind以及执行环境

290 阅读8分钟

学了很久js,开始的时候自己最容易搞蒙的就是函数的abc三个方法:apply、bind、call。对于一些进阶应用,比如组合式继承,柯里化,不掌握这三个方法就很难理解复杂的用法。而js的执行环境,包括v8引擎的优化,之前写的笔记也是单独用了一部分篇幅。索性就将这两部分整理到一起写了。(建议先了解闭包)

犀牛书核心参考部分的例子(p766):

apply:

  1. Object.prototype.toString.apply(o);

  2. var data=[2,24,6,123];
    Math.max.apply(null,data);
    

bind:

es5新增方法。 假设f是一个函数,调用bind()方法如下:

var g=f.bind(o,1,2);

这样g就成为了一个新函数。调用g(3)等价于:

f.call(o,1,2,3);//后面会提到,bind这种调用方式其实就是柯里化。

call:

Object.prototype.toString.call(o);

apply和call是调用一个函数,bind是返回一个函数。

以前看犀牛书 没发现这么多错误,总共三个示例印错两处。

看完了这部分直接跳到柯里化,以前费很大劲才能理解的内容,这次一下子懂了。主要是因为过去基础不扎实,对arguments的理解就不到位,arguments是属于拥有它的函数的,之前讲的执行环境创建变量对象里有三件事:arguments创建,函数声明,变量声明。还有对于词法作用域的理解。后来闭包看多了就习惯了。。。

柯里化

柯里化函数的写法:

function curry(fn){
  var args=Array.prototype.slice.call(arguments,1);
  return function(){
    var innerarg= Array.prototype.slice.call(arguments);
    var finalarg=args.concat(innerarg);
    return fn.apply(null,finalarg);
  }
}

通过闭包,把传入的第一部分参数固定住。以后再调用,传入剩余的参数就可以了。通过这种形式,可以把参数分成两部分,一部分固定参数,一部分是柯里化后表面上需要引用的参数。

function add(num1,num2){
  return num1+num2;
}
var curriedAdd=curry(add,3);
console.log(curriedAdd(5));//8

或者

function add(num1,num2){
  return num1+num2;
}
var curriedAdd=curry(add,3,5);
console.log(curriedAdd());//8

再复杂点的是下面的bind()函数

function bind(fn,context){
  var args=Array.prototype.slice.call(arguments,2);
  return function(){
    var innerarg=Array.prototype.slice.call(arguments);
    var finalarg=args.concat(innerarg);
    return fn.apply(context,finalarg);
  }
}

函数和绑定对象,还有参数提前传入。

使用方式如下:

var needbindfunc=bind(fn,contextobj,arg1,arg2);
needbindfunc(arg);

这算是函数绑定和柯里化的一个结合。 对于红皮书(js高程)p606的示例,EventUtil源代码在p354页。

偶然发现js高程发行第四版了。。。中文版估计会抢手一波,必入!相比犀牛、js高程看起来更舒服。

var handler = {
    message: "Event handled",

    handleClick: function(name, event){
        alert(this.message + ":" + name + ":" + event.type);
    }
};

var btn = document.getElementById("my-btn");
addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn"));
 

addHandler: function (element, type, handler) {
    if (element.addEventListener) {        //DOM2级
        element.addEventListener(type, handler, false);//event会传入handler内

    } else if (element.attachEvent) {      //DOM1级
        element.attachEvent("on" + type, handler);

    } else {
        element["on" + type] = handler;    //DOM0级
    }
}

addHandler分别接受三个参数,dom元素、事件类型、处理函数

bind返回柯里化后的只接受event参数的函数。”my-btn”通过闭包和其他参数一起进入到函数。

es5的bind方法也实现了柯里化。

尝试模拟es5的bind实现:

Function.prototype.bind=function(context){
    var args=Array.prototype.slice.call(arguments,1);
    var that=this; 
    return function(){
        var innerarg=Array.prototype.slice.call(arguments);
        var finalarg=args.concat(innerarg);
        return that.apply(context,finalarg);
    }
}
var g=f.bind(o,1,2);

其实这么写存在一些问题,实际应用中,可能需要进行一些基本判断,比如调用bind的函数对象是否合法,原型链继承,还有关于this指向的问题。 假如我们这么调用,通常我们希望g是f绑定传递的对象和参数后的实现。如果把f看成构造函数,f就可能有自己的原型方法,那么理应继承到f的原型。通过上面我写的bind函数是无法继承的。因此,需要额外实现继承f的原型。

//转载自网络
if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5 internal IsCallable function  
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var aArgs = Array.prototype.slice.call(arguments, 1),
      fToBind = this,
      fNOP = function () {},
      fBound = function () {
        return fToBind.apply(this instanceof fNOP ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)));
      };
    if (this.prototype) {
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();
    return fBound;
  };
}

return fToBind.apply(this instanceof fNOP ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)));

首个参数为什么出现this instanceof fNOP ?this : oThis || window?这涉及到构造函数和this机制的问题。 这个英文解释说的非常清晰:stackoverflow.com/questions/5…

上一篇笔记举过一个例子

var o = {
    m: function(){
        return this;
    }
}
var obj=new o.m();
console.log(obj,obj === o);//{} false
console.log(obj.constructor === o.m);

当函数作为构造函数调用时,则this是构造函数返回的对象。

上面bind例子的简化版本

Function.prototype.bind = function( obj ) {
  var self = this,
  nop = function () {},
  bound = function () {
    return self.apply( this instanceof nop ? this : obj, arguments );
  };

  nop.prototype = self.prototype;
  bound.prototype = new nop();

  return bound;
};

对于上面的bind调用测试:

var obj = {};
function foo(x) {
  this.answer = x;
}
var bar = foo.bind(obj);   // "always" use obj for "this"    
bar(42); 
console.log(obj.answer);   // 42
var other = new bar(1);    // Call bar as a constructor     
console.log(obj.answer);   // Still 42
console.log(other.answer); // 1

当使用new bar()时,实际上就相当于是bind方法里的new bound(),这种情况this instanceof nop等于true,bar绑定的就是构造对象。正常调用bar()时,则绑定之前的上下文对象。

函数的简单赋值,往往会丢失原来的对象,this没办法指向正确的内容。如不进行处理,会导致运行出错。bind就能很容易的解决这个问题。

自己实现的还有一个问题:

function Point(x, y) {  
  this.x = x;  
  this.y = y;
} 
Point.prototype.toString = function() {  
   console.log(this.x + ',' + this.y);
}; 
var p = new Point(1, 2);
p.toString(); // '1,2'  
var emptyObj = {};
var YAxisPoint = Point.bind(null, 0/*x*/); 
var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5' axisPoint instanceof Point; // true axisPoint instanceof YAxisPoint; // true
console.log(new Point(17, 42) instanceof YAxisPoint); //true

最后一行代码输出为true,当然,这是想当然需要的结果,但是看不到v8里bind的源代码,所以就有点困惑。而且,换成上面自我实现的bind写法,这个地方会是false。

一个无限柯里化的例子:

//转载于网络
function add() {
    var args =[].slice.call(arguments);
    return function () {
        var inargs = [].slice.call(arguments);
        if( arguments.length == 0 ){
            var me = 0 ;
            for(var i in args){
                me +=  args[i];
            }
            return me ;
        }
        else
            return add.apply(null, args.concat(inargs));
            //上面这一句说明最后是返回自身,因此可以持续柯里化
    };
}
add(1)(2)();//3

bind()也可以为需要特定this值的函数创造捷径。

例如要将一个类数组对象转换为真正的数组,可能的例子如下:

var slice = Array.prototype.slice; 
// ...
slice.call(arguments);

如果使用bind()的话,情况变得更简单:

var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(unboundSlice);
 // ... 
slice(arguments);

反柯里化

反柯里化就是把

o.method(p1,p2);这种调用方式转变为method(o,p1,p2);。这样带来的好处,就是扩大了对象方法的适用范围,可以方便的进行方法借用。而函数是可以作为值传递的,所以又由此会引申出很多其他的调用方式。怎么解决从o.method(p1,p2)到method(o,p1,p2)呢?

Function.prototype.uncurrying = function() {
  var that = this;
  return function() {
    return Function.prototype.call.apply(that, arguments);
  }
};

其实反柯里化的写法也有好多种,也可以这么写:

return that.apply(arguments[0],[].shift.call(arguments));

看起来这种方式更扁平不那么绕。

调用如下:

var o={
  value:"tiedan",
  sayHi:function(){
    return "Hello " + this.value +" "+[].slice.call(arguments);
  }
};

var sayHiuncurrying=o.sayHi.uncurrying();
console.log(sayHiuncurrying({value:'world'},"hahaha"));

和调用call相比:

o.sayHi.call({value:'world'},"hahaha") 在调用方式上方便了。尤其需要大量这种调用的情况。?仔细思考一下反柯里化的转变形式。

其实把uncurring封装成函数更好:

var uncurrying= function (fn) {
  return function () {
    return fn.apply(arguments[0],[].shift.call(arguments));
  }
};

执行环境

对于执行上下文来说,它的生命周期由三部分组成:

  1. 创建:变量对象,作用域链,this指向
  2. 执行:变量对象变为活动对象,可以访问。即可赋值,函数引用,或其他代码操作。
  3. 执行完毕:被推出栈,等待GC

对于函数的创建变量对象部分,有三部分内容, arguments,函数声明,变量声明。

  1. arguments参数对象创建,是个类数组结构
  2. 函数声明可用新的引用覆盖掉之前同名的声明。
  3. 变量声明如果重复则会跳过。如果没有初始化的变量,统一给undefined。

由二三点可得出一个结论,函数的优先级比变量高,并且变量是无法覆盖掉同名函数声明的。

v8引擎

转载:v8引擎为什么这么快

v8引擎是用c++编写的开源项目,同时是可以和chrome剥离的。业界最快的js引擎。据说比firefox和safari快10倍,比ie快50倍。

JIT技术是v8的一种加速方式,代码不转换中间字节码,直接转机器码。V8是将源代码进行词法分析和语法分析,得到抽象语法树,然后直接将抽象语法树编译成机器代码。

快速属性访问,用到隐藏类和内嵌缓存技术。可以减少访问hash表,提高访问速度。

动态机器码生成,转换为机器码后,还需要动态的对代码进行优化处理。处理回滚等。

高效的垃圾收集,与java的hotspot类似的机制,增量标记+延迟清理

隐藏类要强调属性初始化赋值的顺序,顺序错了,就会剔除优化,影响效率。所以在习惯上要适应这一点!

虽然经常说js是一门解释型语言,但是js发展到今天,为了处理越来越复杂的应用。例如一些office应用都搬到浏览器环境了。为了提高运行效率。处理js的引擎也有了很大的进步。对于v8和其他一些js引擎来说,处理js代码都会进行编译。

转载:V8引擎及优化技术

转载:JavaScript V8引擎