资讯专栏INFORMATION COLUMN

JavaScript设计模式

keke / 2511人阅读

摘要:可能因为先入为主,在编程之中,往往不由自主地以的逻辑编程思路设计模式进行开发。这是原型模式很重要的一条原则。关于闭包与内存泄露的问题,请移步原型模式闭包与高阶函数应该可以说是设计模式的基础要领吧。在下一章,再分享一下的几种常用设计模式。

在学习使用Javascript之前,我的程序猿生涯里面仅有接触的编程语言是C#跟Java——忽略当年在大学补考了N次的C与VB。

从静态编程语言,转到动态语言JS,刚开始的时候,遇到不少困难与错误。可能因为先入为主,在JS编程之中,往往不由自主地以C#的逻辑、编程思路、设计模式进行JS开发。

时间长了,渐渐发现JS设计模式对于前端开发的重要性,但在分享JS设计模式之前,希望首先分享几点个人觉得很重要的JS设计模式基础。有些地方,可能研究得也不是很深入,有什么错误的话,望指正。

动态类型语言

静态类型语言,在编译阶段,就需要确定变量的类型。但动态类型语言,只有程序运行时,根据当时语言执行环境,变量才会赋予其类型。

所以C#,不能抛开runtime与编译器多带带运行,Java也离不开JVM。同时也有一大堆问题需要考虑的,跨平台、类型转换、反射、Ioc、接口编程、抽象化、动态代理等等一大堆概念技术的产生,还不是因为静态语言的强类型特性,需要更多的手段让其得到灵活多态的特性。

    public class Main
    {
        public void Function()
        {
            //在编译的时候,.net runtime必须要知道dog是一只狗
            Dog dog = new Dog();
            Console.WriteLine(dog.Name);
        }
    }

但,JS只需要个文本编辑器,没有编译通过不通过的概念,加载了浏览器就能跑。

    var a = 1;
    console.log(typeof a);//number

    a = "a";
    console.log(typeof a);//string
原型模式

ECMAScript 5之前JS是没有类的,W3C的大牛是有所发现的,所以ES6会新的类语法,但是各大浏览器(IE)猴年马月才跟进就不得而知了——绝对不是针对IE。

在静态类型语言开发中,类是任意实体的结构,当需要这个实体的时候,系统依照这个结构,“画”出这个==对象==。
譬如,要设计车,首先是要设计稿,设计稿就是类,然后照着设计稿生产的每一台车就是具体的对象。
所以,静态类型语言当中,得到一个对象的最关键就是先有这个对象的设计稿(类),就是知道它的类型。

但是,JS不一样。
JS是以原型模式作为设计基础。得到一个对象之前,我们不关心对象属于什么类型,它的设计稿是怎么样的,JS是通过拷贝一个其他对象而获得对象的。

    //定义超人是超人,超人可以飞
    var superman = {
        name: "superman",
        fly: function () {
            console.log(this.name + " is flying");
        }
    };
    superman.fly();//superman is flying

    //万一,想要个妹子呢,搞个女超人如何?女超人也会飞?要不就复制一个超人的对象吧。
    //通过Object.create复制一个超人,但定义它是女超人。
    var superwomen = Object.create(superman);

    superwomen.name = "superwomen";
    superwomen.fly();//superwomen is flying

    //DC的不喜欢,来个漫威吧,再搞个钢铁侠?钢铁侠也会飞
    var ironman = Object.create(superman);

    ironman.name = "ironman";
    ironman.fly();//ironman is flying

千万别误解了superman就是类,而superwomen、ironman就是superman实例化得到的对象,superman、superwomen、ironman都是对象,superwomen、ironman都是superman对象拷贝的结果。

1、所有的数据都是对象

上面的例子,通过var obj={}创建对象,其等价于var obj=new Object()。Object对象可以创建对象,但不仅是Object才是对象,所有的数据都是对象,包括Number、Boolean、String、Function、Object。这是原型模式很重要的一条原则。
正是这一点,我们也可以利用function定义对象:function obj(){};

不过与var obj=new Object()不同,function定义的对象,是一个带构造器的函数,通过new关键字调用构造器可以获取其原型对象。参考以下例子。

    //定义一个对象
    var superman = {
        name: "superman",
        fly: function () {
            console.log(this.name + " is flying");
        }
    };
    superman.fly();//superman is flying

    //定义一个函数,同时也是一个带构造器的函数
    function superman2() {
        this.name = "superman";
        this.fly = function () {
            console.log(this.name + " is flying");
        };
    };

    //错误:superman2是函数,fly()不是函数
    superman2.fly();//error:undefined is not a function

    //错误:superman2()是调用函数,但函数没有返回带fly()函数的对象
    superman2().fly();//error:Cannot read property "fly" of undefined

    //正确:new不是C#实例化关键字,new是调用superman2的构造器并返回其对象
    var sm = new superman2();
    sm.fly();//superman is flying
2、对象的属性请求,会从对象传递到对象原型

当需要获得对象属性时,会首先请求对象本身,如果,对象本身没有属性可以响应,则响应其对象原型prototype;如果prototype也没有属性可以响应,则进一步请求原型的原型,从而形成一条原型链。当然,原型链的层级不能太长,具体长度没有研究过,我觉得3级也差不多了。

    //定义人
    function human() {
        this.sexy = "male";
    };

    //定义所有英雄的原型
    function heroBase() {
        this.trait = "strong cool";
    };

    //英雄的原型是人
    heroBase.prototype = new human();

    //定义超人
    function superman() {
        this.fly = function () {
            console.log("superman");
        };
    };

    //超人的原型就是英雄
    superman.prototype = new heroBase();

    console.log(new superman().sexy);//male
    console.log(new superman().trait);//strong cool

这个步骤就是:

超人的性别是神马?

光看超人不知道,要看超人原型:英雄

看英雄也不知道,要看英雄原型:人

人的sexy属性告诉我们,他是男人

好吧,也许你发现了,上面例子是有bug的,超人是外星人;但你肯定发现了,这不就是继承吗?继承都有了,多态还能远吗?

3、JS就是通过原型模式实现继承的,且是单一继承

我个人的理解是,JS的继承跟C#的继承虽然实现形式不同,但有一点原则是相同的,单一继承非多继承。如上例子换成以下代码:
superman.prototype = new heroBase();
superman.prototype = new human();
最终结果是,超人的原型首先定为英雄,但又被人所覆盖了。符合单一继承的原则。

4、特别补充:关于__proto__prototype的区别

之前一直以为搞懂了原型模式,最近看到一篇很好文章 [学习笔记] 小角度看JS原型链 梦禅,涨姿势了。

在 segmentfault 上看到这样一道题目:

var F = function(){};
Object.prototype.a = function(){};
Function.prototype.b = function(){};
var f = new F();

问:f 能取到a,b吗?原理是什么?

关键理解:
prototype是对象原型
__proto__是对象构造器的原型
关于两者区别,推荐一遍博文,解释非常详细,理解js中的原型链,prototype与__proto__的关系

自己列了例子,应该比较容易理解

    var F = function () { };
    Object.prototype.a = function () {
        console.log("a");
    };
    Function.prototype.b = function () {
        console.log("b");
    };
    var f = new F();

    //注意:对象的__proto__属性,指向对象的父级构造器的prototype原型

    console.log(f);

    console.log(f.__proto__);                         //F.prototype
    console.log(F.prototype.__proto__);               //Ojbect.prototype
    console.log(Object.prototype.a());                //a

    console.log(f.__proto__.__proto__.a());           //a

    /****************************************************************/

    console.log(f.constructor);                       //F
    console.log(F.__proto__);                         //Function.prototype
    console.log(Function.prototype.b());              //b

    console.log(f.constructor.__proto__.b());         //b

    //结论:所有对象的__proto__都指向其构造器的prototype
    //结论:所有构造器/函数的__proto__都指向Function.prototype,它是一个空函数(Empty function)
    //结论:Function.prototype的__proto__最终指向Object.prototype

所有对象的__proto__都指向其构造器的prototype

所有构造器/函数的__proto__都指向Function.prototype,它是一个空函数(Empty function)

Function.prototype的__proto__最终指向Object.prototype

多态

JS的多态是通过两点实现的:1、原型模式实现继承;2、动态类型实现泛型

回想一下,我们刚学习C#的那个经典例子

    public class Hero
    {
        public virtual void Fly()
        {
            Console.WriteLine("hero fly");
        }
    }

    public class SuperMan : Hero
    {
        public override void Fly()
        {
            base.Fly();
            Console.WriteLine("SuperMan fly");
        }
    }

    public class IronMan : Hero
    {
        public override void Fly()
        {
            base.Fly();
            Console.WriteLine("IronMan fly");
        }
    }

    public class Main
    {
        public void MainFunction()
        {
            LetsFly(new SuperMan());
            LetsFly(new IronMan());
        }

        public void LetsFly(Hero hero)
        {
            hero.Fly();
        }
    }

再来看看JS的实现

    function superman() {
        this.fly = function () {
            console.log("superman fly");
        };
    }

    function bird() {
        this.fly = function () {
            console.log("bird fly");
        };
    };

    function letsFly(hero) {
        hero.fly();
    }

    var sm = new superman();
    var b = new bird();
    letsFly(sm);//superman fly
    letsFly(b);//bird fly;

LetsFly()方法,C#跟JS接受的同样都是Hero这个对象

对于C#而言,LetsFly方法要求不管是神马Hero,只要实现了基类Hero的Fly,它就能执行LetsFly方法,以后即使有再多的英雄,继承Hero即可。

而JS,LetsFly方法要求不管是神马对象,只要有Fly方法,它就能执行LetsFly方法,以后即使有再多的英雄,英雄会Fly即可。

对比之下,优缺点是显而易见的,前者受到束缚,尤其在复杂类型之间继承与引用提现的更加凸显,但类型安全性高;

后者无拘无束,但类型安全性低,如上例子,LetsFly传入的可能不是Hero,很可能是Bird。

闭包与高阶函数

关于闭包“closure”,刚开始接触的时候,百度了N遍相关教程还是看得我一头雾水,
听说还会有内存泄露的问题,好吧,在开发中干脆就不用吧。直到有一天,偶尔看到高阶函数,对于闭包以及整个JS语言的理解可以说产生了翻天覆地的变化。

在这之前,先给大家回顾一下,闭包之所以成为闭包之前,有一个很重要的前提概念:变量的生命周期。
在函数体内,var定义的变量,变量属于函数内,函数外无法访问;反之如果没有定义var的变量,像x=12,x属于全局变量。
全局变量的生命周期是永久的,由页面加载到关闭;而内部变量,从函数执行到结束,GC检测到内部变量没有再被使用则会销毁它。

    var fn = function () {
        var a = "a";
        b = "b";

        var fni = function () {
            c = "c";
        }();
    }();

    //console.log(a);//a is not defined
    console.log(b);//b
    console.log(c);//c

关键就在这,如果内部变量在函数执行结束后,仍然有被使用呢?

解决方案就是——高阶函数

高阶函数名字看起来高端,实际很简单,满足以下条件其一就是高阶函数:

1、函数的参数是函数;

2、函数的返回值是函数;

    function myFunction() {
        console.log("我是个函数");
    };
    //高阶函数1——函数的参数是函数
    function fnHO1(fn) {
        fn();
    }
    //高阶函数2——函数的返回值是函数
    function fnHO2() {
        return myFunction;
    };

    //调用高阶函数1
    fnHO1(myFunction);//我是个函数

    //调用高阶函数2
    fnHO2();//神马都没有发生

    //调用高阶函数2,获得的是它的内部函数体
    var f2 = fnHO2();
    //再执行,才是调用它的内部函数
    f2();//我是个函数

在高阶函数的基础,我们来看看,史上最经典的闭包实例。

    function add_outer() {
        var i = 1;

        //返回值是内部函数,inner调用了其外部的变量
        //所以inner执行结束时,i没有被销毁
        return function inner() {
            i++;
            return i;
        };
    };

    //执行add_outer获取的是它的内部函数体inner,但没有执行
    var inner = add_outer();

    //真正执行的时候,变量i没有被销毁,形成递增
    console.log(inner());//2
    console.log(inner());//3
    console.log(inner());//4

我对于闭包的理解很简单:==函数返回值是个内部函数,内部调用了函数的内部变量==。

两个条件,缺一不可。如果将以上例子改一下,内部有函数,但不是返回值,就不是闭包了。

    function add_outer() {
        var i = 1;

        //返回值不是函数,形成不了闭包
        function inner() {
            i++;
            console.log(i);
        };
        inner();
    };

    //执行多次,值也不会变
    add_outer();//2
    add_outer();//2

也许大家会疑问,既然内部变量i在函数结束后仍然使用,导致GC无法回收其内存,那不就是内存泄露吗?
是的。其实我们反过来想,如果我们不使用闭包的方式实现以上累加的例子,改为使用全局变量存放变量i,全局变量i是否同样也是不能被回收呢?!

    //function add_outer() {
        var i = 1;

        function inner() {
            i++;
            return i;
        };
    //};

    ////执行add_outer获取的是它的内部函数体inner,但没有执行
    //var inner = add_outer();

    //真正执行的时候,变量i没有被销毁,形成递增
    console.log(inner());//2
    console.log(inner());//3
    console.log(inner());//4

因此,闭包与高阶函数,只是函数式编程的编写方式,即使并不是造成内存泄露的原因。关于闭包与内存泄露的问题,请移步 http://www.cnblogs.com/yakun/p/3932026.h...

原型模式、闭包与高阶函数应该可以说是JS设计模式的基础要领吧。在下一章,再分享一下JS的几种常用设计模式。

还是像前面所说的,有什么地方说错,望大家指正。

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

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

相关文章

  • JS程序

    摘要:设计模式是以面向对象编程为基础的,的面向对象编程和传统的的面向对象编程有些差别,这让我一开始接触的时候感到十分痛苦,但是这只能靠自己慢慢积累慢慢思考。想继续了解设计模式必须要先搞懂面向对象编程,否则只会让你自己更痛苦。 JavaScript 中的构造函数 学习总结。知识只有分享才有存在的意义。 是时候替换你的 for 循环大法了~ 《小分享》JavaScript中数组的那些迭代方法~ ...

    melody_lql 评论0 收藏0
  • 2017年 最好的javascript 书籍

    摘要:请记住,这些书中的一些可能不是最新的,但概念和基础仍应适用。是最好的老师之一。的秘密由部分组成。在你完成这些书后,查看书籍和最好的本土书籍。 我看过三本,第1本,第二本,第四本。第一本买的的实体书,其他两本看的是电子书。第一本是大名鼎鼎老道写的,书很薄,但是非常经典。javascirpt忍者秘籍是jquery的作者写的,也是非常经典。you dont kown js系列也是非常好。看了...

    mingzhong 评论0 收藏0
  • 前端练级攻略(第二部分)

    摘要:是文档的一种表示结构。这些任务大部分都是基于它。这个实践的重点是把你在前端练级攻略第部分中学到的一些东西和结合起来。一旦你进入框架部分,你将更好地理解并使用它们。到目前为止,你一直在使用进行操作。它是在前端系统像今天这样复杂之前编写的。 本文是 前端练级攻略 第二部分,第一部分请看下面: 前端练级攻略(第一部分) 在第二部分,我们将重点学习 JavaScript 作为一种独立的语言,如...

    BWrong 评论0 收藏0
  • 理解JavaScript的核心知识点:原型

    摘要:首先,需要来理清一些基础的计算机编程概念编程哲学与设计模式计算机编程理念源自于对现实抽象的哲学思考,面向对象编程是其一种思维方式,与它并驾齐驱的是另外两种思路过程式和函数式编程。 JavaScript 中的原型机制一直以来都被众多开发者(包括本人)低估甚至忽视了,这是因为绝大多数人没有想要深刻理解这个机制的内涵,以及越来越多的开发者缺乏计算机编程相关的基础知识。对于这样的开发者来说 J...

    iKcamp 评论0 收藏0
  • JavaScript系列(四) - 收藏集 - 掘金

    摘要:函数式编程前端掘金引言面向对象编程一直以来都是中的主导范式。函数式编程是一种强调减少对程序外部状态产生改变的方式。 JavaScript 函数式编程 - 前端 - 掘金引言 面向对象编程一直以来都是JavaScript中的主导范式。JavaScript作为一门多范式编程语言,然而,近几年,函数式编程越来越多得受到开发者的青睐。函数式编程是一种强调减少对程序外部状态产生改变的方式。因此,...

    cfanr 评论0 收藏0
  • JavaScript进阶之路

    摘要:前端入门的门槛相对较低,学习曲线是越来越陡峭,由浅入深,可以分为四个阶段。第二阶段高级程序设计有的书是用来成为经典的,比如犀牛书还有些书是用来超越经典的,显然这本书就是。接下来可以看看教程,看看源代码,尝试着写一写这些效果。 前端入门的门槛相对较低,学习曲线是越来越陡峭,由浅入深,可以分为四个阶段。 第一阶段:《JavaScript DOM编程艺术》    看这本书之前,请先确认你对J...

    Lowky 评论0 收藏0

发表评论

0条评论

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