zoukankan      html  css  js  c++  java
  • 《JavaScript高级程序设计》笔记:高级技巧

    高级函数

    安全的类型检测

    在任何值上调用Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性就指定了上述字符串中的构造函数名。

    var arr = [];
    function fn(){
        
    }
    var reg = /^d/;
    var json = {
        "name":"Jack",
        "age":20
    ,}
    console.log(Object.prototype.toString.call(arr) == "[object Array]");  //true
    console.log(Object.prototype.toString.call(fn) == "[object Function]"); //true
    console.log(Object.prototype.toString.call(reg) == "[object RegExp]");  //true
    console.log(window.JSON && Object.prototype.toString.call(json) == "[object Object]"); //true

    作用域安全的构造函数

    作用域安全的构造函数在进行任何更改前,首先确认this对象是正确类型的实例。如果不是,会创建新的实例并返回,如下例子:

    function Person(name,age,job){
        if(this instanceof Person){
            this.name = name;
            this.age = age;
            this.job = job;
        }else{
            return new Person(name,age,job);
        }
    }
    var person1 = Person("jack",29,"IT");
    console.log(window.name); //""
    console.log(person1.name);//jack
    
    var person2 = Person("Tom",20,"teacher");
    console.log(person2.name);//Tom

    如下例子,Rectangle实例中没有添加sides属性:

    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,2);
        this.width = width;
        this.height = height;
        this.getArea = function(){
            return this.width * this.height;
        }
    }
    
    var rect = new Rectangle(5,10);
    console.log(rect.sides); //undefined

    修改后,Rectangle实例中添加了sides属性:

    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,2);
        this.width = width;
        this.height = height;
        this.getArea = function(){
            return this.width * this.height;
        }
    }
    Rectangle.prototype = new Polygon();
    var rect = new Rectangle(5,10);
    console.log(rect.sides); //2

    惰性载入函数

    function createXHR(){
        if(typeof XMLHttpRequest != "undefined"){
            return new XMLHttpRequest();
        }else if(typeof ActiveXObject != "undefined"){
            if(typeof arguments.callee.activeXString != 'string'){
                var verisions = ["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 = version[i];
                        break;
                    }catch(ex){
                        //跳过
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        }else{
            throw  new Error("NO XHR object available.");
        }
    }

    每次调用createXHR()时,他都要对浏览器所支持的能力仔细检查。如果if语句不必每次执行,那么代码可以运行的更快一些。解决方案称之为惰性载入的技巧。

    惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式,第一种就是函数在被调用时再处理函数。在第一次调用的过程中,该函数会覆盖为另外一个按合适方式执行的函数,这样对原函数的调用都不用在经过执行的分支了。例如上面例子重写为:

    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 verisions = ["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 = version[i];
                            break;
                        }catch(ex){
                            //跳过
                        }
                    }
                }
                return new ActiveXObject(arguments.callee.activeXString);
            }    
        }else{
            createXHR = function(){
                throw  new Error("NO XHR object available.");
            }
        }
        return 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 verisions = ["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 = version[i];
                            break;
                        }catch(ex){
                            //跳过
                        }
                    }
                }
                return new ActiveXObject(arguments.callee.activeXString);
            }    
        }else{
            return function(){
                throw  new Error("NO XHR object available.");
            }
        }
    })();

     函数绑定

    var handler = {
        message:"Event handler",
        handleClick:function(event){
            console.log(this.meesage); //undefined
        }
    }
    var btn = document.getElementsByClassName("my-btn")[0];
    btn.addEventListener("click",handler.handleClick,false);

    上面的结果貌似会显示“Event handler”的结果,但是结果是undefined,是因为没有保存handler.handleClick()的环境。所以this对象最后是指向了DOM按钮,而非handler对象(在IE8中,this指向window)。可以使用一个闭包来修正这个问题,如下代码:

    var handler = {
        message:"Event handler",
        handleClick:function(event){
            console.log(this.message); //Event handler
        }
    }
    var btn = document.getElementsByClassName("my-btn")[0];
    btn.addEventListener("click",function(event){
        handler.handleClick(event);
    },false);

    很多javascript库实现了一个可以将函数绑定到指定环境的函数。这个函数一般都叫做bind()。

    一个简单的bind()函数接收一个函数和一个环境。并返回一个在给定函数中调用给定函数的函数,并且将所有参数原封不动的传递过去。语法如下:

    function bind(fn,context){
        return function(){
            return fn.apply(context,arguments);
        }
    }

    那么我们就可以用上面的bind()方法来实现绑定,如下代码:

    var handler = {
        message:"Event handler",
        handleClick:function(event){
            console.log(this.message); //Event handler
        }
    }
    var btn = document.getElementsByClassName("my-btn")[0];
    btn.addEventListener("click",bind(handler.handleClick,handler),false);

    ECMAScript5为所有函数定义了一个原生的bind()方法,那么上面代码可以如下:

    var handler = {
        message:"Event handler",
        handleClick:function(event){
            console.log(this.message); //Event handler
        }
    }
    var btn = document.getElementsByClassName("my-btn")[0];
    btn.addEventListener("click",handler.handleClick.bind(handler),false);

    原生的bind()方法和上面自定义的bind()方法很相似,都要传入作为this值的对象。支持原生bind()方法的浏览器有IE9+、Firefox4+和Chrome。

     函数柯里化

    与函数绑定紧密相关的是主题是函数柯里化(function curring),它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数,如下例子:

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

     尽管从技术上来说curriedAdd()并非柯里化的函数,但它很好的展示了其概念。

    柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。下面是创建柯里化函数的通用方式:

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

    调用方式:

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

    也可像下面方式调用:

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

    函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更为复杂的bind()函数,如下:

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

    实例:

    var handler = {
        message:"Event handled",
        handleclick:function(name,event){
            console.log(this.message + ":" + name + ":" + event.type);
        }
    }
    var btn = document.getElementById('my-btn');
    btn.addEventListener('click',bind(handler.handleclick,handler,"my-btn"),false);  //Event handled:my-btn:click

    ECMAScript5的bind()方法也实现了函数的柯里化,如下代码:

    var handler = {
        message:"Event handled",
        handleclick:function(name,event){
            console.log(this.message + ":" + name + ":" + event.type);
        }
    }
    var btn = document.getElementById('my-btn');
    btn.addEventListener('click',handler.handleclick.bind(handler,"my_btn"),false);  //Event handled:my-btn:click

     防篡改对象

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

    不可扩展对象Object.preventExtensions()

    使用Object.preventExtensions()就不能给原对象添加新的属性和方法了,如下例子:

    var person = {
        name:"jack"
    };
    Object.preventExtensions(person);
    person.age = 20;
    console.log(person.age); //undefined  严格模式下抛出异常

    使用Object.isExtensible()确定对象是否可以扩展,如下:

    var person = {
        name:"jack"
    };
    console.log(Object.isExtensible(person)); //true
    
    Object.preventExtensions(person);
    console.log(Object.isExtensible(person)); //false

    密封的对象Object.seal()

    密封对象不可扩展,而且已有成员的[[configurable]]特性将被设置为false。这就意味着不能删除属性和方法,因为不能使用Object.defineProperty()把数据属性修改为访问器属性,或者相反。属性值是可以修改的。

    var person = {
        name:"jack"
    };
    Object.seal(person);
    person.age = 20;
    console.log(person.age); //非严格模式下:undefined  严格模式下:抛出异常
    
    delete person.name;
    console.log(person.name); //非严格模式下:jack   严格模式下:抛出异常

    使用Object.isSealed()方法可以确定对象是否被密封了。因为被密封的对象不可扩展,所以调用Object.isExtensible()检测密封的对象也会返回false。

    var person = {
        name:"jack"
    };
    console.log(Object.isExtensible(person)); //true
    console.log(Object.isSealed(person)); //false
    
    Object.seal(person);
    console.log(Object.isExtensible(person));//false
    console.log(Object.isSealed(person));//true

    冻结的对象Object.freeze()

    最严格的防篡改级别是冻结对象。冻结的对象既不可扩展,又是密封的,而且对象数据属性[[writable]]特性会被设置为false。如果定义[[Set]]函数,访问器属性仍然是可写的。

    ECMAScript5定义的Object.freeze()方法可以用来冻结对象,如下代码:

    var person = {
        name:"jack"
    };
    Object.freeze(person);
    
    person.age = 20;
    console.log(person.age); //非严格模式:undefined  严格模式下:抛出异常
    
    delete person.name;
    console.log(person.name);//非严格模式:jack  严格模式下:抛出异常
    
    person.name = "Tom";
    console.log(person.name);//非严格模式:jack  严格模式下:抛出异常

    使用Object.isFrozen()方法检测冻结对象。因为冻结对象既是密封的又是不可扩展的,所以调用Object.isExtensible()和Object.isSealed()方法分别返回false和true。

    var person = {
        name:"jack"
    };
    
    console.log(Object.isExtensible(person)); //true
    console.log(Object.isSealed(person)); //false
    console.log(Object.isFrozen(person)) //false
    
    Object.freeze(person);
    console.log(Object.isExtensible(person));//false
    console.log(Object.isSealed(person));//true
    console.log(Object.isFrozen(person)) //true

     高级定时器

    函数节流

    DOM操作比起非DOM交互需要更多的内存和CPU。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。比如在IE浏览器中使用onresize事件处理程序的时候容易发生,当调整浏览器窗口的时候,该事件会连续发生。在onresize事件处理程序内部如果尝试进行DOM操作,其高频率的更改可能会让浏览器崩溃。为了解决这个问题,可以使用定时器对该函数进行节流。

    基本形式代码结构如下:

    var processor = {
        timeoutId:null,
        
        //实际进行处理的方法
        performProcessing:function(){
             //实际执行的代码
        },
        
        //初始处理调用的方法
        process:function(){
            clearTimeout(this.timeoutId);
            var that = this;
            this.timeoutId = setTimeout(function(){
                that.performProcessing();
            },100);
        }
    }
    
    //尝试开始 执行
    processor.process();

    时间间隔设置为了100ms,这表示最后一次调用process()之后,至少100ms后才会调用performProcessing()。所以如果100ms之内调用了20次process(),也只会调用performProcessing()一次。

    这个模式可以使用throttle函数来简化,这个函数可以自动进行定时器的设置和清除,如下代码:

    function throttle(method,context){
        clearTimeout(method.tId);
        method.tId = setTimeout(function(){
            method.call(context);
        },100);
    }

    throttle()函数接受两个参数:要执行的函数以及在哪个作用域中执行。

    来看个例子,假如有一个div元素需要保持它的高度始终等于宽度。那么实现这个JS代码如下:

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

    上面代码有两个问题可能造成浏览器运行缓慢,一个是计算offsetWidth属性,如果该元素或者页面上的其它元素有非常复杂的css样式,那么这个过程将会很复杂。 另一个设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的CSS的话,这有需要很多计算。这就可以用到throttle()函数,如下代码:

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

    只要代码是周期性执行的,都应该使用节流。

    自定义事件

    事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其它对象可以观察该对象,等等这些有趣的时刻到来并通过运行代码来响应。

    观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即便观察者不存在。从另一个方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,你的事件处理代码便是观察者。

    事件是与DOM交互的最常见的方式,但它们也可以用于非DOM代码中--通过实现自定义事件。

    自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以如下定义:

    function EventTarget(){
        this.handlers = {};
    }
    EventTarget.prototype = {
        constructor:EventTarget,
        addHandler:function(type,handler){
            if(typeof this.handlers[type] == "undefined"){
                this.handlers[type] = [];
            }
            this.handlers[type].push(handler);
        },
        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);
                }
            }
        },
        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);
            }
        }
    };

    EventTarget类型有一个单独的属性handlers,用于存储事件处理程序。
    定义的三个方法如下:

    • 1.addHandler:用于注册给定类型事件的事件处理程序;该方法接受两个参数,事件类型和用于处理该事件的函数。
    • 2.fire:触发一个事件;该方法接受一个单独的参数,是一个至少包含type属性的对象。fire()方法先给event对象设置一个target属性,如果它尚未被指定的话。然后它就查找对应该事件类型的一组处理程序,调用各个函数,并给出event对象。因为这些都是自定义事件,所以event对象上还需要的额外信息由你自己决定。
    • 3.removeHandler:注销某个事件类型的事件处理程序;它接受的参数跟addHandler是一样的。

    使用EventTarget类型的自定义事件可以如下使用:

    function  handlerMessage(event){
        console.log("Message received:" + event.message);
    }
    
    //创建一个新对象
    var target = new EventTarget();
    
    //添加一个事件处理程序
    target.addHandler("message",handlerMessage);
    
    //触发事件
    target.fire({type:"message",message:"Hello world!"});
    
    //移除事件处理程序
    target.removeHandler("message",handlerMessage);
    
    //再次,应没有处理程序
    target.fire({type:"message",message:"Hello world!"});

     如下实例:

    function object(o){
        function F(){};
        F.prototype = o;
        return new F();
    }
    
    function inheritPrototype(subType,superType){
        var prototype = object(superType.prototype);
        prototype.constructor = subType;
        subType.prototype = prototype;
    }
    
    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});
    }

    Person类型使用了寄生组合继承方法来继承EventTarget。怎样使用:

    function  handlerMessage(event){
        console.log(event.target.name + " says:" + event.message);
    }
    
    //创建新Person
    var person = new Person("Jack",29);
    
    //添加一个事件处理程序
    person.addHandler("message",handlerMessage);
    
    //在该对象上调用一个方法,它触发消息事件
    person.say('Hi there');

     拖放

    拖放功能

    下例代码自己添加了一部分控制在屏幕区域的代码,如下:

    var DragDrop = function(){
        var dragging = null,
                differX = 0,
                differY = 0,
                targetWidth = 0,
                targetHeight = 0,
                windowWidth = 0,
                windowHeight = 0,
                _isMove = false; //是否移动
        
        function handleEvent(event){
            //获取事件和目标
            event = event || window.event;
            var target = event.target || event.srcElement;
            
            //确认事件类型
            switch(event.type){
                case "mousedown":
                    if(target.className.indexOf("drggable") != -1){
                        dragging = target;
                        _isMove = true;
                        differX = event.clientX - target.offsetLeft;
                        differY = event.clientY - target.offsetTop;
                        targetWidth = target.offsetWidth;
                        targetHeight = target.offsetHeight;
                        windowWidth = document.documentElement.clientWidth;
                        windowHeight = document.documentElement.clientHeight;
                    }
                    break;
                case "mousemove":
                    if(dragging !== null && _isMove){
                        var left = event.clientX - differX,
                                top = event.clientY - differY;
                        if(left < 0){
                            left = 0;
                        }
                        else if(left > windowWidth - targetWidth){
                            left = windowWidth - targetWidth;
                        }
                        
                        if(top < 0){
                            top = 0;
                        }
                        else if(top > windowHeight - targetHeight){
                            top = windowHeight - targetHeight;
                        }
                        
                        dragging.style.left = left  + "px";
                        dragging.style.top = top + "px";
                    }
                    break;
                case "mouseup":
                    dragging = null;
                    _isMove =false;
                    break;
            }
        }
        
        //公共接口
        return {
            enable:function(){
                document.addEventListener("mousedown",handleEvent,false);
                document.addEventListener("mousemove",handleEvent,false);
                document.addEventListener("mouseup",handleEvent,false);
            },
            disable:function(){
                document.removeEventListener("mousedown",handleEvent,false);
                document.removeEventListener("mousemove",handleEvent,false);
                document.removeEventListener("mouseup",handleEvent,false);
            }
        }
        
    }();
    DragDrop.enable();

    添加自定义事件

     上面写的拖放功能还不能真正应用起来,除非能知道什么时候拖动开始了。从这点来看,前面的代码没有提供任何方法表示拖动开始、正在拖动或者拖动结束。这时,可以使用自定义事件来指示这几个事件的发生,让应用的其它部分和拖动功能进行交互。如下代码:

    function EventTarget(){
        this.handlers = {};
    }
    EventTarget.prototype = {
        constructor:EventTarget,
        addHandler:function(type,handler){
            if(typeof this.handlers[type] == "undefined"){
                this.handlers[type] = [];
            }
            this.handlers[type].push(handler);
        },
        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);
                }
            }
        },
        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);
            }
        }
    };
    
    
    var DragDrop = function(){
        var dragdrop = new EventTarget(),
                dragging = null,
                differX = 0,
                differY = 0;
        
        function handleEvent(event){
            //获取事件和目标
            event = event || window.event;
            var target = event.target || event.srcElement;
            
            //确认事件类型
            switch(event.type){
                case "mousedown":
                    if(target.className.indexOf("drggable") != -1){
                        dragging = target;
                        _isMove = true;
                        differX = event.clientX - target.offsetLeft;
                        differY = event.clientY - target.offsetTop;
                        dragdrop.fire({type:"dragstart",target:dragging,x:event.clientX,y:event.clientY})
                    }
                    break;
                case "mousemove":
                    if(dragging !== null){
                        //指定位置
                        dragging.style.left = (event.clientX - differX)  + "px";
                        dragging.style.top = (event.clientY - differY) + "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,false);
                document.addEventListener("mousemove",handleEvent,false);
                document.addEventListener("mouseup",handleEvent,false);
            };
            dragdrop.disable = function(){
                document.removeEventListener("mousedown",handleEvent,false);
                document.removeEventListener("mousemove",handleEvent,false);
                document.removeEventListener("mouseup",handleEvent,false);
            };
            return dragdrop;    
    }();

    这段代码定义了三个自定义事件:dragstart、drag、dragend,它们都将被拖动的元素设置为了target,并给出了x和y属性来表示当前的位置。调用如下:

    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 + ")";
    });

    贴出HTML代码:

    <div id="status"></div>
    <div id="myDiv" class="drggable"></div>

    CSS代码:

    *{margin:0;padding:0;}
    #myDiv{width:200px;height:200px;background: blue;position: absolute;top:50px;left:400px;}
  • 相关阅读:
    springSecurity登陆与退出json形式交互
    SQL-Mysql表结构操作
    SQL-Mysql数据类型
    SQL-SQL事物操作
    springboot之Validation参数校验
    springSecurity之java配置篇
    springsecurity入门篇
    springboot集成shiro
    13个不low的JS数组操作
    基于canvas的五子棋
  • 原文地址:https://www.cnblogs.com/moqiutao/p/10244231.html
Copyright © 2011-2022 走看看