资讯专栏INFORMATION COLUMN

js编程中经常遇到的一些问题(持续更新)

willin / 1325人阅读

摘要:一前言本文适合有一定开发基础的读者,文章涉及开发中经常遇到的一些令人疑惑的问题,理解这些问题有助于我们快速提升对这门语言的理解和应用能力。

一:前言

本文适合有一定JS开发基础的读者,文章涉及开发中经常遇到的一些令人疑惑的问题,理解这些问题有助于我们快速提升对JS这门语言的理解和应用能力。文章只讲述具体问题中的关键问题,不涵盖全面的知识点。如想了解具体的知识,可以参考笔者博客的相关文章。

二:正文 1.丢失的this

在实际应用中, this的指向大致分为以下四种:
(1)作为对象方法的调用
(2)作为普通函数调用
(3)构造器调用
(4)Function.prototype.call或Function.prototype.apply

1-1阅读下面代码:

//1.作为对象方法的调用this总是指向那个对象
window.name = "globalName";
var getName = function(){
    return this.name;
};
console.log( getName() ); // 输出:globalName
//2.作为普通函数的调用:非严格模式下this总是指向window,严格模式下 undefined
window.name = "globalName";
var myObject = {
    name: "sven",
    getName: function(){
        return this.name;
    }
};
var getName = myObject.getName;//关键:这里保留了一个普通函数的引用
console.log( getName() ); // globalName

通过以上两个对比,理解使用方法不同,this指向不同

1-2阅读下面的代码:

var getId = function( id ){
    return document.getElementById( id );
};
getId( "div1" );

//我们也许思考过为什么不能用下面这种更简单的方式:
var getId = document.getElementById;
getId( "div1" );

document.getElementById方法需要用到this。这个this本来被期望指向document,当getElementById被当作 document的属性被调用时,方法内部的this确实是指向document.
但是当使用getId来引用document.getElementById之后,在调用getId,此时就变成了普通函数调用,内部的this就指向了window。
利用call或者apply更正this指向:
//我们可以尝试利用apply 把document 当作this 传入getId 函数,帮助“修正”this:

document.getElementById = (function( func ){
    return function(){
        return func.apply( document, arguments );
    }
})( document.getElementById );

var getId = document.getElementById;
var div = getId( "div1" );
alert (div.id); // 输出: div1
2.实现手动绑定this

2-1:bind方法的兼容写法

var bind = Function.prototype.bind || function( context ){
        var self = this; // 保存原函数
        return function(){ // 返回一个新的函数
            return self.apply( context, arguments ); // 执行新的函数的时候,会把之前传入的context当作新函数体内的this
        }
    };
3.闭包

3-1.现在来看看下面这段代码:

var func = function(){
    var a = 1;
    return function(){
        a++;
        alert ( a );
    }
};

var f = func();

f(); // 输出:2
f(); // 输出:3
f(); // 输出:4
f(); // 输出:5

当执行f = func()时,f返回了一个匿名函数的引用,它可以访问到func()被调用时产生的环境,而局部变量a一直处在这个环境里。这个变量就有了不被销毁的理由,这里就产生了一个闭包结构。
3-2常见的闭包的问题:




    



    
1
2
3
4
5

3-3.利用闭包延续局部变量的寿命

//img 对象经常用于进行数据上报,如下所示:
    var report = function( src ){
    var img = new Image();
        img.src = src;
    };
    report( "http://xxx.com/getUserInfo" );
//丢失数据的原因是img是report函数中的局部变量,当函数调用之后局部变量就销毁了,而此时或许还没来得及发起http请求
    //现在我们把img 变量用闭包封闭起来,便能解决请求丢失的问题:
    var report = (function(){
        var imgs = [];
        return function( src ){
            var img = new Image();
            imgs.push( img );
            img.src = src;
        }
    })();

闭包与内存管理
闭包会使一些数据无法被及时的销毁,如果将来需要回收这些变量,我们可以手动把这些变量设置为null。
跟闭包和内存泄漏有关系的地方是,使用闭包的同时容易形成循环引用,如果闭包的作用域链中保存着一些DOM结点,这时候就有可能造成内存泄漏。

4.高阶函数

(1)函数可以作为参数被传递
(2)函数可以作为返回值输出
4-1.函数作为参数传递
Array.prototype.sort方法:

    var array = ["10","5","12","3"];
    array.sort();
    //array:["10","12","3","5"]
    //如代码那样,排序的结果并不是我们想要的,这与sort函数的比较规则有关系
    array.sort(function(a,b){return a-b;});
    //array:["3","5","10","12"]
    传入一个比较的函数,就可以按照数字大小的规则进行正确的比较了。

4-2.函数作为返回值输出

var getSingle = function ( fn ) {
        var ret;
        return function () {
            return ret || ( ret = fn.apply( this, arguments ) );
        };
    };

4-3.函数作为参数被传递并且返回另一个函数

var getScript = getSingle(function(){
        return document.createElement( "script" );
    });
    var script1 = getScript();
    var script2 = getScript();
    alert ( script1 === script2 ); // 输出:true

4-4.高阶函数应用
(1)高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些业务逻辑无关的功能包括日志统计、控制安全、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。
下面代码通过扩展Function.prototype来实现把一个函数“动态织入”

Function.prototype.before = function( beforefn ){
        var __self = this; // 保存原函数的引用
        return function(){ // 返回包含了原函数和新函数的"代理"函数
            beforefn.apply( this, arguments ); // 执行新函数,修正this
            return __self.apply( this, arguments ); // 执行原函数
        }
    };

    Function.prototype.after = function( afterfn ){
        var __self = this;
        return function(){
            var ret = __self.apply( this, arguments );
            afterfn.apply( this, arguments );
            return ret;
        }
    };

    var func = function(){
        console.log( 2 );
    };

    func = func.before(function(){
        console.log( 1 );
    }).after(function(){
        console.log( 3 );
    });

func();

(2)柯里化
一个currying函数首先会接受一些参数,接受了这些参数之后,该函数不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存了下来。待到函数真正需要求值的时候,之前传入的所有参数都会一次性用于求值。

一个经典的柯里化:

function curry(fn){
        var arr1 = Array.prototype.slice.call(arguments,1);
        return function(){
            var arg2 = Array.prototype.slice.call(arguments);
            var array = arr1.concat(arr2);
            return fn.apply(null,array);
        }
    }

不断累积的柯里化:

   var currying = function( fn ){
        var args = [];//外层函数变量:用来累积
        return function(){
            if ( arguments.length === 0 ){
                return fn.apply( this, args );
            }else{
                [].push.apply( args, arguments );
                return arguments.callee;
            }
        }
    };

(3)uncurrying

在javascript中,当我们调用对象的某个方法时,其实不用关心对象原本是否被设计为拥有这个方法,这是动态类型语言的特点,也就是常说的鸭子类型思想。
同理,一个对象也未必只能使用它自己的方法,其实可以借用原本不属于他的方法: call apply

Function.prototype.uncurrying = function () {
        var self = this;
        return function() {
            var obj = Array.prototype.shift.call( arguments );
            return self.apply( obj, arguments );
        };
    };

    var push = Array.prototype.push.uncurrying();
var obj = {
    "length": 1,
    "0": 1
};

push( obj, 2 );//将2使用push的方法作用到obj上
console.log( obj ); // 输出:{0: 1, 1: 2, length: 2}
5.函数节流

函数节流也用到了高阶函数的知识,因为比较重要,所以单开了一个标题。
javascript中的函数在大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理。但是在一些少数情况下,函数可能被很频繁的调用,而造成大的性能问题。
(1)函数被频繁调用的场景

1.window.onresize事件
2.mousemove事件
3.上传进度

(2)函数节流的原理
解决函数触发频率太高的问题,需要我们按照时间段来忽略一些事件请求。
(3)函数节流的代码实现
详情可以参考
Underscore.js#throttle
Underscore.js#debounce
简单实现:
将即将被执行的函数用steTimeout延时一段时间执行。如果该次延时执行还没有完成,就忽略掉接下来调用该函数的请求。

var throttle = function ( fn, interval ) {
        var __self = fn, // 保存需要被延迟执行的函数引用
        timer, // 定时器
        firstTime = true; // 是否是第一次调用
        return function () {
            var args = arguments,
            __me = this;
            if ( firstTime ) { // 如果是第一次调用,不需延迟执行
                __self.apply(__me, args);
                return firstTime = false;
            }
            if ( timer ) { // 如果定时器还在,说明前一次延迟执行还没有完成
                return false;

            timer = setTimeout(function () { // 延迟一段时间执行
                clearTimeout(timer);
                timer = null;
                __self.apply(__me, args);
            }, interval || 500 );
        };
    };


    window.onresize = throttle(function(){
        console.log( 1 );
    }, 500 );

另一种实现函数节流的方法-分时函数

某些函数确实是用户主动调用的,但是因为一些客观的原因,这些函数会严重的影响页面的性能。
一个例子就是创建QQ好友列表。如果一个好友列表用一个节点表示,当我们在页面中渲染这个列表的时候,可能要一次性的网页面中创建成百上千个节点。

var ary = [];
for ( var i = 1; i <= 1000; i++ ){
    ary.push( i ); // 假设ary 装载了1000 个好友的数据
};

var renderFriendList = function( data ){
    for ( var i = 0, l = data.length; i < l; i++ ){
        var div = document.createElement( "div" );
        div.innerHTML = i;
        document.body.appendChild( div );
    }
};

renderFriendList( ary );

在短时间内网页面中大量添加DOM节点显然也会让浏览器吃不消。
这个问题的解决方案之一是下面的timeChunk函数:让创建节点的工作分批进行

//第一个参数是创建节点时需要的数据,第二个参数封装了创建节点逻辑的函数,第三个参数表示每一批创建节点的数量。
var timeChunk = function( ary, fn, count ){
    var obj,
    t;
    var len = ary.length;
    var start = function(){
        for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){
            var obj = ary.shift();
            fn( obj );
        }
    };
    return function(){
        t = setInterval(function(){
        if ( ary.length === 0 ){ // 如果全部节点都已经被创建好
            return clearInterval( t );
        }
        start();
        }, 200 ); // 分批执行的时间间隔,也可以用参数的形式传入
    };
};

var ary = [];
for ( var i = 1; i <= 1000; i++ ){
    ary.push( i );
};
var renderFriendList = timeChunk( ary, function( n ){
    var div = document.createElement( "div" );
    div.innerHTML = n;
    document.body.appendChild( div );
}, 8 );
renderFriendList();
6.惰性加载函数

在web开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免。

var addEvent = function( elem, type, handler ){
        if ( window.addEventListener ){
            return elem.addEventListener( type, handler, false );

        }
        if ( window.attachEvent ){
            return elem.attachEvent( "on" + type, handler );
        }
    };

这个函数的缺点是,当它每次被调用的时候都会执行里面的if条件分支。
下面这个函数虽然仍然有一些分支判断,但是在第一次进入条件分支之后,在函数内部就会重写这个函数,重写之后的函数就是我们希望的addEvent函数。

  var addEvent = function(ele,type,handler){
        if(window.addEventListener){
            addEvent = function(ele,type,handler){
                elem.addEventListener( type, handler, false );
            }
        }
        if(window.attachEvent){
            addEvent = function(ele,type,handler){
                elem.attachEvent( "on" + type, handler );
            }
        }
        addEvent(ele,type,handler);
    }
三:结语

文章介绍的都是JS需要掌握的重点又是难点的知识,需要多动手实践才能理解。有关相关知识的详细讲解,可以参考笔者的相关文章。当然 ,最好的方式是去谷歌然后自己动手实践。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82784.html

相关文章

  • [译] 如何恰当地学习 JavaScript

    摘要:原文链接恰当地学习适合第一次编程和非的程序员持续时间到周前提无需编程经验继续下面的课程。如果你没有足够的时间在周内完成全部的章节,学习时间尽力不要超过周。你还不是一个绝地武士,必须持续使用你最新学到的知识和技能,尽可能地经常持续学习和提高。 原文链接:How to Learn JavaScript Properly 恰当地学习 JavaScript (适合第一次编程和非 JavaSc...

    Jason 评论0 收藏0
  • 前端开发负责人修炼指北

    摘要:大家好,我叫,江湖人称吃土小叉,目前担任公司的前端负责人半年多了,一路上摸爬滚打,历经团队人员变动,近日颇有感触,于是结合自己近半年的前端负责人实践经验,权当作一个学习记录,整理归纳一下小作坊团队前端负责人的修炼要点大部分只是记录了关键词, 大家好,我叫XX,江湖人称吃土小2叉,目前担任公司的前端负责人半年多了,一路上摸爬滚打,历经团队人员变动,近日颇有感触,于是结合自己近半年的前端负...

    Drummor 评论0 收藏0
  • React.js 常见问题

    摘要:我们常常会收到一些有趣的问题,但大多数问题都是常见问题。我创建这个资源为了帮助学习者遇到这些常见的问题时提供一定帮助。这些是表示没有任何子节点的元素的标记。不绑定处理程序方法我把这个留到最后,因为这是一个大问题,一个很常见的问题。 在 jsComplete,我们管理一个专门用于帮助编程学习者 slack 帐户。我们常常会收到一些有趣的问题,但大多数问题都是常见问题。 我创建这个资源为了...

    KitorinZero 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<