前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript高级技巧

JavaScript高级技巧

作者头像
奋飛
发布2019-08-15 09:57:23
1.1K0
发布2019-08-15 09:57:23
举报
文章被收录于专栏:Super 前端Super 前端

下述内容主要讲述了《JavaScript高级程序设计(第3版)》第22章关于“高级技巧”。

一、高级函数

函数是第一等公民,所有函数都是对象。

1. 安全的类型检测

JavaScript内置的类型检测机制并非完全可靠。

代码语言:javascript
复制
var isArray = value instanceof Array;

以上代码要返回true,value必须是一个数组,而且还必须与Array构造函数在同个全局作用域中。(Array是window的属性)如果value是在另外一个iframe中定义的数组,上述代码则返回false。 注意:BOM的核心对象时window,它表示浏览器的一个实例。在浏览器中,window对象有双重角色,它既是通过JavaScript访问浏览器窗口的一个接口,又是ECMAScript规定的global对象。 解决上述问题: Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。

代码语言:javascript
复制
function isArray(value) {
    return Array.isArray(value) || Object.prototype.toString.call(value) == "[object Array]"; 
}

function isType(type) {
    return function(obj) {
        return Object.prototype.toString.call(obj) == "[object " + type + "]"
    }
}
var isObject = isType("Object")
var isArray = Array.isArray || isType("Array")

注意:Object.prototype.toString()本身可能被修改!

2. 作用域安全的构造函数
代码语言:javascript
复制
function Person(name, age) {
    this.name = name;
    this.age = age;
}

当使用new调用时,构造函数内用到的this对象会指向新创建的对象实例。

代码语言:javascript
复制
var p1 = new Person("lg", 26);
console.log(p1.name, p1.age);       // lg 26

到不使用new调用时,由于this是在运行时绑定的,直接调用Person(),this会映射到全局对象window上,导致错误对象属性的意外增加。

代码语言:javascript
复制
var p2 = Person("camile", 26);
console.log(p2.name, p2.age);       // TypeError: Cannot read prototype 'name'
console.log(window.name, age);      // camile 26

注意:由于window的name属性用于识别链接目标和iframe的,上述覆盖可能会导致严重的问题。

解决上述问题:作用域安全构造函数

代码语言:javascript
复制
function Person(name, age) {
    if(this instanceof Person) {
        this.name = name;
        this.age = age;
    } else {
        return new Person(name, age);
    }
}

var p3 = Person("camile", 26);      // 这里没有使用new操作符
console.log(p3.name, p3.age);       // camile 26

通过上述模式,意味着锁定了可以调用构造函数的环境。如果使用构造函数窃取模式继承且不使用原型链,会破坏整个继承

代码语言:javascript
复制
function Polygon(sides) {
    if(this instanceof Polygon) {
        this.sides = sides;
        this.getArea = function() {
            return 0;
        }
    }else {
        return new Polygon(sides);
    }
}

function Rectangle(width, height) {
    Polygon.call(this, 4);
    this.width = width;
    this.height = height;
    this.getArea = function() {
        return this.width * this.height;
    }
}

var rect = new Rectangle(2, 4);
console.log(rect.getArea());    // 8
console.log(rect.sides);    // undefined 

Polygon.call(this, 4)用于Polygon构造函数是作用域安全的,this并非Polygon的实例,所以会创建并返回一个新的Polygon对象,并没有实际作用,this得不到增长,所以Rectangle实例中不会有sides属性。

解决上述问题:使用原型模式或者寄生模式 方式一:原型模式

代码语言:javascript
复制
Rectangle.prototype = new Polygon();
var rect = new Rectangle(2, 4);
console.log(rect.getArea());    // 8
console.log(rect.sides);    // 4

方式二:寄生模式

代码语言:javascript
复制
function Rectangle(width, height) {
    var r = new Polygon(4); 
    r.width = width;
    r.height = height;
    r.getArea = function() {
        return r.width * r.height;
    }
    return r;
}

var rect = new Rectangle(2, 4);
console.log(rect.getArea());    // 8
console.log(rect.sides);    // 4

注意: 构造函数内部创建的var r = new Polygon(4)与外部创建没有什么不同。不能依赖instanceof来确定对象类型。

代码语言:javascript
复制
rect instanceof Rectangle;  // false
rect instanceof Polygon;    // true

由于存在上述问题,建议在可以使用其他模式的情况下,不要使用这种模式。

3. 惰性载入函数

可以将任和代码分支推迟到第一次调用函数的时候。 因浏览器之间行为差异,多数JavaScript代码包含大量的if语句。 例如,创建兼容性的XMLHttpRequest对象【Ajax与Comet

代码语言:javascript
复制
/* 兼容IE早期版本 */
function createXHR(){
    if (typeof XMLHttpRequest != "undefined"){
        return new XMLHttpRequest();
    } else if (typeof ActiveXObject != "undefined"){    // 适用于IE7之前的版本
        if (typeof arguments.callee.activeXString != "string"){
            var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                            "MSXML2.XMLHttp"],
                i, len;

            for (i=0,len=versions.length; i < len; i++){
                try {
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString = versions[i];
                    break;
                } catch (ex){
                    //skip
                }
            }
        }

        return new ActiveXObject(arguments.callee.activeXString);
    } else {  // XHR对象和ActiveX对象都不存在,则抛出错误 
        throw new Error("No XHR object available.");
    }
}

如果浏览器中支持内置XHR对象,每次if判断测试就显得多余了!!! 通过惰性载入的技术可以很好的解决上述问题。 方式一:在函数被调用时再处理函数

代码语言:javascript
复制
function createXHR(){
    if (typeof XMLHttpRequest != "undefined"){
        createXHR = function(){
            return new XMLHttpRequest();
        };
    } else if (typeof ActiveXObject != "undefined"){
        createXHR = function(){                    
            if (typeof arguments.callee.activeXString != "string"){
                var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                                "MSXML2.XMLHttp"],
                    i, len;

                for (i=0,len=versions.length; i < len; i++){
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                    } catch (ex){
                        //skip
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        };
    } else {
        createXHR = function(){
            throw new Error("No XHR object available.");
        };
    }
    // 调用上述新函数
    return createXHR();
}

方式二:在声明函数时就指定适当的函数

代码语言:javascript
复制
// 自执行,在createXHR声明时,就为其指定了相关创建方法。
var createXHR = (function(){
    if (typeof XMLHttpRequest != "undefined"){
        return function(){
            return new XMLHttpRequest();
        };
    } else if (typeof ActiveXObject != "undefined"){
        return function(){                    
            if (typeof arguments.callee.activeXString != "string"){
                var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                                "MSXML2.XMLHttp"],
                    i, len;

                for (i=0,len=versions.length; i < len; i++){
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                        break;
                    } catch (ex){
                        //skip
                    }
                }
            }

            return new ActiveXObject(arguments.callee.activeXString);
        };
    } else {
        return function(){
            throw new Error("No XHR object available.");
        };
    }
})();

补充1. 惰性单例

代码语言:javascript
复制
var getSingle = function(fn) {
    var result;
    return function() {
        return result || (result = fn.apply(this, arguments));
    };
};
// 测试
function testSingle(){}
getSingle(testSingle)() === getSingle(testSingle)();    // true

2. 系统中提供的页面刷新【回调函数不支持参数】

代码语言:javascript
复制
var refreshPage = (function () {
    var fun;
    function register(callback) {
        fun = callback;
    }
    return function (callback) {
        callback && typeof callback === 'function' ? register(callback) : fun();
    }
})();

// 改写
var refreshPage = (function() {
    var fn;
    return function(callback) {
        callback && typeof callback === 'function' ? fn = callback : fn(); 
    }
})();
4. 函数绑定

函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。

代码语言:javascript
复制
var handler = {
    message: "Event handled",
    handleClick: function(event) {
        console.log(this.message);
    }
};

document.getElementById("btn").addEventListener("click", handler.handleClick);      // undefined
问题在于没有保存handleClick()的环境,this被绑定到了当前DOM按钮上。

// 闭包修正
document.getElementById("btn").addEventListener("click", function() {
    handler.handleClick(event);
});
代码语言:javascript
复制
// 自定义bind方法
function bind(fn, context) {
    return function() {
        return fn.apply(context, arguments);
    };
}
document.getElementById("btn").addEventListener("click", bind(handler.handleClick, handler));
代码语言:javascript
复制
// ES5新增bind方法 Function.prototype.bind
document.getElementById("btn").addEventListener("click", handler.handleClick.bind(handler));
5. 函数柯里化

用于创建已经设置好了一个或多个参数的函数。 其基本方法和函数绑定是一样的:使用一个闭包返回一个函数。 二者区别在于:当函数被调用时,返回的函数还需设置一些传入的参数。

代码语言:javascript
复制
function curry(fn){
    // 获取参数,并转化为数组(第一个参数为柯里化的函数)
    var args = Array.prototype.slice.call(arguments, 1);
    return function(){
        // 获取参数,并转化为数组
        var innerArgs = Array.prototype.slice.call(arguments),
            // 所有参数集(最终,有效参数受限于fn)
            finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);
    };
}

function add(num1, num2){
    return num1 + num2;
}

var curriedAdd = curry(add, 5);
console.log(curriedAdd(3));   //8

var curriedAdd2 = curry(add, 5, 12);
console.log(curriedAdd2());   //17

var curriedAdd3 = curry(add, 5, 12);
console.log(curriedAdd3(1));   //17
代码语言:javascript
复制
/* 增强版bind */
function bind(fn, context){
    var args = Array.prototype.slice.call(arguments, 2);
    return function(){
        var innerArgs = Array.prototype.slice.call(arguments),
            finalArgs = args.concat(innerArgs);
        return fn.apply(context, finalArgs);
    };
}

注意:和上述“函数绑定”中的“自定义bind方法”区分

代码语言:javascript
复制
function enhanchBind(fn, context){
    var args = Array.prototype.slice.call(arguments, 2);
    return function(){
        var innerArgs = Array.prototype.slice.call(arguments),
            finalArgs = args.concat(innerArgs);
        return fn.apply(context, finalArgs);
    };
}
document.getElementById("btn").addEventListener("click", enhanchBind(handler.handleClick, handler, event, "函数柯里化"));

二、防篡改对象

一旦把对象定义为防篡改,就无法撤销了。

preventExtensions –> seal –> freeze isExtensible –> isSealed –> isFrozen

1. 不可扩展对象

默认情况下,所有对象都是可扩展的。意味着,任何时候都可以向对象中添加属性和方法。

代码语言:javascript
复制
var person = {name: "lg"};
person.age = 26;
Object.preventExtensions(person);
person.address = "sd";
console.log(person);    // Object {name: "lg", age: 26} address属性没有成功添加
console.log(Object.isExtensible(person));   // false,不可扩展
2. 密封的对象

密封对象不可扩展,而且已有成员的[[configurable]]特性将被设置为false。意味着,不能删除属性和方法。

代码语言:javascript
复制
var person = {name: "lg"};
Object.seal(person);
person.age = 26;
console.log(person);    // Object {name: "lg"} age属性没有成功添加
console.log(Object.isExtensible(person));   // false,不可扩展
console.log(Object.isSealed(person));   // true,密封的
3. 冻结的对象

冻结对象不可扩展、又是密封的,而且已有成员的[[Writable]]特性将被设置为false。如果定义了[[Set]]函数,访问器属性仍可写。

代码语言:javascript
复制
var person = {
    name: "lg",
    _address: "sd"  // 表示私有
};
Object.defineProperty(person, "address", {   // 访问器属性
    get: function() {
        return this._address;
    },
    set: function(add) {
        this._address = add;
    }
})
Object.freeze(person);
person.age = 26;
console.log(person);    // Object {name: "lg", address: "sd"} age属性没有成功添加
person.name = "ligang";
person.address = "shandong";    // ??????

console.log(Object.isExtensible(person));   // false,不可扩展
console.log(Object.isSealed(person));   // true,密封的
console.log(Object.isFrozen(person));   // true,冻结的

对于JavaScript库的作者而言,冻结对象是很有用的,其很好的防止了意外修改库中核心对象。

4. 总结
这里写图片描述
这里写图片描述

三、高级定时器

JavaScript运行于单线程的环境中,而定时器仅仅只是计划代码在未来的某个时间执行。执行时机不能保证。 定时器对队列的工作方式是,当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它立即执行,而是能表示它会尽快执行。设定一个150ms后执行的定时器不代表了150ms代码就立刻执行,它表示代码会在150ms后被加入到队列中。如果,在这个时间点上,队列中没有其他东西,那么这段代码就会被执行,表面看上去好像就在精确指定的时间点上执行了。其他情况下,代码可能明显等待更长时间才执行。

谨记:定时器指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。

1. 重复的定时器

setInterval(),JavaScript引擎“仅当没有该定时器的任何代码实例时“,才将定时器代码添加到队列。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。 其会存在两个问题:(1)某些间隔会被跳过;(2)多个定时器的代码执行之间的间隔可能会比预期的小。

假如,某个onclick事件处理程序使用setInterval()设置了一该处理是否个200ms间隔的重复定时器。如果事件处理程序花了300ms多一点的时间完成,同时定时器代码也花了差不多的时间,就会同时出现跳过间隔且连续运行定时器代码的情况。

代码语言:javascript
复制
btn.addEventListener("click", function(){
    // 300ms时间执行
    setInterval(function(){
        // 执行需要300ms
    }, 200);
});
JavaScript进程时间线
JavaScript进程时间线

解释:第一个定时器在205ms处添加到队列中,但是直到过了300ms处才能够执行。当执行这个定时器代码时,在405ms处又给队列添加了另外一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中。结果在5ms处添加的定时器代码结束后,405ms处添加的定时器代码就立即执行。

解决上述问题:链式调用setTimeout()

代码语言:javascript
复制
setTimeout(function(){
    // 处理中
    setTimeout(arguments.callee, interval)
}, interval);

好处:在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,可以保证在下一次定时器代码执行之前,至少等待指定的间隔,避免了连续的运行。

2. Yielding Processes

运行在浏览器中的JavaScript都被分配了一个确定数量的资源,当页面中存在一个耗时较大的脚本时,会导致浏览器卡死。 此时需要考虑两个问题:(1)该处理是否必须同步完成?(2)数据是否必须按顺序完成? 如果都是“否”,则需要考虑“数组分块”技术。

代码语言:javascript
复制
/**
 * @array 要处理的项目的数组
 * @process 处理项目的函数
 * @context 运行函数的环境
 */
function chunk(array, process, context) {
    setTimeout(function(){
        var item = array.shift();
        process.call(context, item);
        if(array.length > 0) {
            setTimeout(arguments.callee, 100);
        }
    }, 100);
}

var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342];
function printValue(item){
    var div = document.getElementById("myDiv");
    div.innerHTML += item + "<br>";        
}

chunk(data, printValue); // 函数在全局中,所以无需传入执行环境
console.log(data);       // []

注意:传递给chunk()的数组在处理数据时,数组中的条目也在改变。如果想保持原数组保持不变,应该将数组进行克隆。

代码语言:javascript
复制
var data2 = [1, 2, 3];
chunk(data2.concat(), printValue);
console.log(data2);     // [1, 2, 3]
3. 节流处理

在浏览器中,处理DOM交互需要更多的内存和CUP时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,甚至崩溃。 函数节流背后的基本思想是指:某些代码不可以在没有间断的情况连续重复执行。 目的:只有在执行函数的请求停止了一段时间之后才执行。

代码语言:javascript
复制
/**
 * @param method 方法
 * @param scope 当前函数执行作用域
 */
function throttle(method, scope) {
    clearTimeout(method.tId);
    method.tId= setTimeout(function(){
        method.call(scope);
    }, 100);
}

function resizeDiv(){
    var div = document.getElementById("myDiv");
    div.style.height = div.offsetWidth + "px";
}

// 节流在resize事件中最常用
window.onresize = function(){
    throttle(resizeDiv);
};

注意:上述函数是一个debounce,而不是一个throttle。debounce与throttle区别

四、自定义事件

事件是一种叫做观察者的设计模式,是一种创建松散耦合代码的技术。 对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应。 观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者订阅这些事件来观察该主体。 JavaScript设计模式–观察者模式

代码语言:javascript
复制
/* 管理事件的对象 */
function EventTarget(){
    this.handlers = {};    
}

EventTarget.prototype = {
    constructor: EventTarget,
    /**
     * 添加事件
     * @param type 事件类型
     * @param handler 事件处理程序
     */
    addHandler: function(type, handler){
        if (typeof this.handlers[type] == "undefined"){
            this.handlers[type] = [];
        }

        this.handlers[type].push(handler);
    },
    /**
     * 触发事件
     * @param event 事件对象
     */
    fire: function(event){
        if (!event.target){
            event.target = this;
        }
        if (this.handlers[event.type] instanceof Array){
            var handlers = this.handlers[event.type];
            for (var i=0, len=handlers.length; i < len; i++){
                handlers[i](event);
            }
        }            
    },
    /**
     * 移除事件
     * @param type 事件类型
     * @param handler 事件处理程序
     */
    removeHandler: function(type, handler){
        if (this.handlers[type] instanceof Array){
            var handlers = this.handlers[type];
            for (var i=0, len=handlers.length; i < len; i++){
                if (handlers[i] === handler){
                    break;
                }
            }
            handlers.splice(i, 1);
        }            
    }
};

实例1:

代码语言:javascript
复制
// 事件处理程序
function handleMessage(event){
    alert("Message received: " + event.message);
}
var target = new EventTarget();
// 添加监听
target.addHandler("message", handleMessage);
// 触发事件
target.fire({ type: "message", message: "Hello world!"});
// 移除监听
target.removeHandler("message", handleMessage);
// 触发事件(无效)
target.fire({ type: "message", message: "Hello world!"});
这里写图片描述
这里写图片描述

实例2:

代码语言:javascript
复制
function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
// 继承    
function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);   //create object
    prototype.constructor = subType;               //augment object
    subType.prototype = prototype;                 //assign object
}

function Person(name, age){
    EventTarget.call(this);
    this.name = name;
    this.age = age;
}

inheritPrototype(Person,EventTarget);

Person.prototype.say = function(message){
    this.fire({type: "message", message: message});
};
// 事件处理程序
function handleMessage(event){
    alert(event.target.name + " says: " + event.message);
}

var person = new Person("Nicholas", 29);
// 监听事件
person.addHandler("message", handleMessage);
// 发布事件
person.say("Hi there.");
这里写图片描述
这里写图片描述

五、拖放 点击某个对象,并按住鼠标按钮不放,将鼠标移动到另一个区域,然后释放鼠标按钮将对象“放”在这里。 拖放的基本概念:创建一个绝对定位的元素,使其可以用鼠标移动。

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<head>
    <title>Drag and Drop Example</title>
</head>
<body>
    <div id="status"></div>
    <div id="myDiv1" class="draggable" style="top:100px;left:0px;background:red;width:100px;height:100px;position:absolute"></div>
    <div id="myDiv2" class="draggable" style="background:blue;width:100px;height:100px;position:absolute;top:100px;left:100px"></div>
    <script type="text/javascript">

        var DragDrop = function(){
            // EventTarget为上述实例中对象
            // 继承EventTarget,具有事件功能
            var dragdrop = new EventTarget(),
                dragging = null,
                diffX = 0,
                diffY = 0;

            function handleEvent(event){

                //get event and target
                var target = event.target;            

                //determine the type of event
                switch(event.type){
                    case "mousedown":
                        if (target.className.indexOf("draggable") > -1){
                            dragging = target;
                            // 保存x、y坐标上的差值
                            diffX = event.clientX - target.offsetLeft;
                            diffY = event.clientY - target.offsetTop;
                            // 发布自定义事件
                            dragdrop.fire({type:"dragstart", target: dragging, x: event.clientX, y: event.clientY});
                        }                     
                        break;

                    case "mousemove":
                        if (dragging !== null){

                            //assign location
                            dragging.style.left = (event.clientX - diffX) + "px";
                            dragging.style.top = (event.clientY - diffY) + "px";   

                            // 发布自定义事件
                            dragdrop.fire({type:"drag", target: dragging, x: event.clientX, y: event.clientY});
                        }                    
                        break;

                    case "mouseup":
                        // 发布自定义事件
                        dragdrop.fire({type:"dragend", target: dragging, x: event.clientX, y: event.clientY});
                        dragging = null;
                        break;
                }
            };

            //全局接口
            // 可以拖放
            dragdrop.enable = function(){
                    document.addEventListener("mousedown", handleEvent);
                    document.addEventListener("mousemove", handleEvent);
                    document.addEventListener("mouseup", handleEvent);
            };
            // 禁止拖放
            dragdrop.disable = function(){
                    document.removeEventListener("mousedown", handleEvent);
                    document.removeEventListener("mousemove", handleEvent);
                    document.removeEventListener("mouseup", handleEvent);
            };

            return dragdrop;
        }();

        DragDrop.enable();

        // 监听(订阅)相关自定义事件                
        DragDrop.addHandler("dragstart", function(event){
            var status = document.getElementById("status");
            status.innerHTML = "Started dragging " + event.target.id;
        });

        DragDrop.addHandler("drag", function(event){
            var status = document.getElementById("status");
            status.innerHTML += "<br>Dragged " + event.target.id + " to (" + event.x + "," + event.y + ")";
        });

        DragDrop.addHandler("dragend", function(event){
            var status = document.getElementById("status");
            status.innerHTML += "<br>Dropped " + event.target.id + " at (" + event.x + "," + event.y + ")";
        });

    </script>
</body>
</html>
这里写图片描述
这里写图片描述
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年05月05日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、高级函数
    • 1. 安全的类型检测
      • 2. 作用域安全的构造函数
        • 3. 惰性载入函数
          • 4. 函数绑定
            • 5. 函数柯里化
            • 二、防篡改对象
              • 1. 不可扩展对象
                • 2. 密封的对象
                  • 3. 冻结的对象
                    • 4. 总结
                    • 三、高级定时器
                      • 1. 重复的定时器
                        • 2. Yielding Processes
                          • 3. 节流处理
                          • 四、自定义事件
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档