javaScript基础(一)

216 阅读14分钟

javaScript基础(一)

· 介绍JavaScript的基本数据类型。

   字符串、数值、对象、布尔值、null、undefined六类基础数据类型。(ES6又新增了第七种类型Symbol值。)

      - 字符串、数值、布尔值合称为原始类型的值

      - null、undefined是特殊类型的值

      - 对象又可分为三类:狭义的对象、数组、函数合成为引用数据类型

· JavaScript原型,原型链 ? 有什么特点?

  原型:javascript的对象都有一个内置的prototype私有属性,这个属性指向的是另一个对象,我们称这个对象为原对象的原型

      - 特点:实现了属性的共享。比如:我们只要把操作方法定义在原型上,当需要时直接调用改方法即可,大大减少了代码冗余,减少内存资源的浪费。

  原型链:原型并不是一个特殊的存在,它也是一个对象,也可以拥有属于它的原型。然后把对象的prototype属性想象成链条,就形成了一条原型链

      - 特点:原型链实现了继承。比如有a、b、c三个对象,a的prototype是b,b的prototype是c,即a可以调用c的属性。

· JavaScript有几种类型的值?你能画一下他们的内存图吗?

  栈:原始数据类型(数值、字符串、布尔值)。

  堆:引用数据类型(数组、函数、对象)。

  总结:

      · 声明变量是不同的内存分配

          - 原始值:存储在栈内的简单数据段,也就是说,它们是直接存储在变量访问的位置。

          - 引用值:存储在堆中的对象,也就是说,存在变量中的值是一个指针,指向存储对象的内存地址。因为引用值的值是会变的,如果放在栈中,会降低变量查寻的速度。相反,如果把对象存储在堆中,存储地址放在栈中,由于存储地址的大小是固定的,所以不会对变量性能造成影响。

      · 复制变量时的不同

          - 原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量完全独立。

          - 引用值:再将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给这个新变量,也就是说两个变量都指向了堆内存中同一个对象,它们中任何一个做出的改变都会反映在另一个身上。

·Javascript如何实现继承?

  原型链继承:将父类型的实例赋值给子类型的原型,则子类型的原型可以访问父类型的属性和方法,以及父类型构造函数中赋值的属性和方法。
    // 声明父类型
    function SuperClass () {
        this.superValue = true
    }
    // 为父类型添加公共方法
    SuperClass.prototype.getSuperValue = function () {
        return this.superValue
    }
    // 声明子类型
    function SubClass () {
        this.subValue = false
    }
    // 将父型类对象的实例赋值给子类型原型(继承父类)
    SubClass.prototype = new SuperClass()
    SubClass.prototype.getSubValue = function () {
        return this.subValue
    }
    // 测试 
    const instance = new SubClass()
    console.log(instance.getSuperValue()) // true
    console.log(instance.getSubValue()) // false
    console.log(instance instanceof SuperClass) // true
    console.log(instance instanceof SubClass) // true
    console.log(SubClass instanceof SuperClass) // false
    console.log(SubClass.prototype instanceof SuperClass) // true
    /** 由于instanceof是判断前面的对象是否是后面类(对象)的实例,所以console.log(SubClass instanceof SuperClass)打印false */

      缺点:

        - 子类型是通过原型prototype对父类型实例化来继承父类。但当父类型中的共有数据是引用类型时,会在子类型中被所有的实例共享,如此一来在一个子类型实例中更改从父类型继承过来的共有属性时,会影响其它子类型。

        - 由于子类是通过原型prototype实例化父类继承的,所以在实例化父类的时候,不能向父类构造函数中传递参数。(如果参数会影响其它对象的实例。)

  借用构造函数继承:即在子类型构造函数的内部调用父类型构造函数;因此可以通过使用apply()和call()方法可以在准备新建的对象上执行的构造函数。如下所示:
    function SuperType () {
        this.colors = [ 'red', 'blue', 'green' ]
    }
    function SubType () {
        // 继承SuperType
        SuperType.call(this)
    }
    var instance1 = new SubType()
    var instance2 = new SubType()
    instance1.colors.push('black')
    console.log(instance1.colors) // ["red", "blue", "green", "black"]
    console.log(instance2.colors) // ["red", "blue", "green"]

      并且解决了向父类构造函数传递参数问题,如下

    function SuperType (name) {
    this.colors = name
    }
    function SubType () {
        // 继承SuperType,同时还传递了参数
        SuperType.call(this, 'javascript')
        this.name = 'sub'
    }
    function SubType2 () {
        // 继承SuperType,同时还传递了参数
        SuperType.call(this, 'javascript2')
    }
    var instance1 = new SubType()
    var instance2 = new SubType2()
    console.log(instance1.colors) // javascript
    console.log(instance1.name) // sub
    // instance1、instance2都继承了SuperType并且都传递了参数,但是它们两个互不影响。

      缺点:

        - 如果仅用借用构造函数,那么无法避免构造函数模式存在的问题-----方法都在函数中定义,那么函数复用就无从谈起了。

  组合继承:指的是原型链和借用构造函数的技术结合一起。其是使用原型链的原型属性和方法的继承、借用构造函数来实现实例属性的继承。如下所示:
    function SuperType (name) {
        this.name = name
        this.colors = ['red', 'blue']
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name)
    }
    
    function SubType (name, age) {
        // 继承SuperType的属性
        SuperType.call(this, name)
        this.age = age
    }
    
    // 继承SuperType方法
    SubType.prototype = new SuperType()
    SubType.prototype.constructor = SubType
    
    SubType.prototype.sayAge = function () {
        console.log(this.age)
    }
    
    var instance1 = new SubType('sub', 13)
    console.log(instance1.colors) // ["red", "blue"]
    console.log(instance1.name) // sub
    console.log(instance1.age) // 13
    instance1.sayName() // sub
    instance1.sayAge() // 13

      优点:

        - 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeof也能识别是基于组合继承创建的对象。

  原型式继承:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。如下所示:
    function object (o) {
        function F () {}
        F.prototype = o
        return new F()
    }
    // object()对传入其中的对象执行了一次浅复制。也就是将传入的对象作为构造函数的原型。
    var person = {
        name: 'hello',
        friends: ['1', '2', '3']
    }
    var sub1 = object(person)
    sub1.name = 'sub1'
    sub1.friends.push('sub1')
    var sub2 = object(person)
    sub2.name = 'sub2'
    sub2.friends.push('sub2')
    console.log(sub1.friends) // ["1", "2", "3", "sub1", "sub2"]
    console.log(sub2.friends) // ["1", "2", "3", "sub1", "sub2"]
    
    // Es5通过新增Object.create()规范了object()方法,这个方法接受两个参数:一个用作新对象原型的对象和一个新对象定义额外属性的对象。
    // 传入一个参数的行为与object()一样
    var person = {
        name: 'hello',
        friends: ['1', '2', '3']
    }
    var sub1 = Object.create(person)
    sub1.name = 'sub1'
    sub1.friends.push('sub1')
    var sub2 = Object.create(person)
    sub2.name = 'sub2'
    sub2.friends.push('sub2')
    console.log(sub1.friends) // ["1", "2", "3", "sub1", "sub2"]
    console.log(sub2.friends) // ["1", "2", "3", "sub1", "sub2"]
    
    // 传入第二个参数,定义额外属性,额外定义的任何属性都会覆盖原型对象上的同名属性。第二个参数与Object.defineProperties()方法的第二个参数格式相同。
    var person = {
        name: 'hello',
        friends: ['1', '2', '3']
    }
    var sub1 = Object.create(person, {
        name: {
            value: 'sub1'
        }
    })
    console.log(sub1.name)
    

      但是引用类型值的属性始终都会共享相应的值,就和原型链一样。

  寄生式继承:是与原型式继承紧密相关的一种思路,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象。
    function object (o) {
        function F () {}
        F.prototype = o
        return new F()
    }
    function createAno (o) {
        var clone = object(o)
        clone.sayHi = function () {
            console.log('hi')
        }
        return clone
    }
    var person = {
        name: 'sub',
        arr: [ '1', '2', '3' ]
    }
    var sub = createAno(person)
    sub.sayHi()
这个例子中的代码基于 person 返回了一个新对象---sub,新对象不仅拥有person的所有属性和方法,也拥有自己的sayHi方法。

      缺点:

        - 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

  寄生组合式继承:即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
    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 SuperType (name) {
        this.name = name
        this.colors = ["red", "blue", "green"]
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name);
    }; 
    
    function SubType (name, age) {
        SuperType.call(this, name)
        this.age = age
    }
    inheritPrototype(SubType, SuperType)
    SubType.prototype.sayAge = function () {
        console.log(this.age);
    }

      组合继承是JavaScript 最常用的继承模式,但会两次调用父类型构造函数(1. SuperType.call(this, name); 2. SubType.prototype);这个例子体现在它只调用了一次 SuperType 构造函数(SuperType.call(this, name)),并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性。

·Javascript创建对象的几种方式?

  构造函数和字面量可以用来创建单个对象。但因为使用一个接口创建很多对象,会产生大量的重复代码。以下几种方式可以解决这个问题。
  工厂模式:是软件工程领域一种广为人知的设计模式,这种模式抽象了创建对象的过程。
    function obj (name, age, job) {
        var o = new Object()
        o.name = name
        o.age = age
        o.job = job
        o.sayName = function () {
            console.log(this.name)
        }
        return o
    }
    var s1 = obj('hello', 12, 'web')
    var s2 = obj('word', 11, 'ui')
    console.log(s1.name) // hello
    console.log(s2.name) // word

      缺点:

        - 工厂模式虽然解决了创建多个相似对象问题,但却没有解决对象的识别问题(就是不知道到底是内置对象、宿主对象、自定义对象)。

  构造函数模式:用构造函数可以创建特定类型的对象。像Object和Array这样的原生构造函数在运行时会自动出现在执行环境中。如下是使用构造函数模式的例子:
    function Person (name, age, job) {
        this.name = name
        this.age = age
        this.job = job
        this.sayName = function () {
            console.log(this.name)
        }
    }
    var s1 = new Person('hello', 12, 'web')
    var s2 = new Person('word', 11, 'ui')
    console.log(s1.name) // hello
    console.log(s2.name) // word
    /**
        这里的函数名的首字母是大写。因为构造函数始终都应该以一个大写字母开头。
        由于要创建Person的新实例,需要用到new操作符。以这种方式调用构造函数会经历以下四个步骤:
        1. 创建一个新对象
        2. 将构造函数中的作用域赋值给新对象(因此this指向了这个新对象)
        3. 执行构造函数中的代码(为这个新对象添加属性)
        4. 返回新对象

        所以s1和s2分别保存着Person的一个不同的实例,但这两个对象都有一个constructor(构造函数)属性,指向的是Person,如下:
    */
   console.log(s1.constructor === Person) // true
   console.log(s2.constructor === Person) // true
   /**
        如果需要检测对象类型就可以用到instanceof操作可以验证(可以验证创建的所有对象是否是某个构造函数的实例:上面创建的对象都是Person、Object的实例)
   */
    

      缺点:

        - 使用构造函数的主要问题,就是每一个方法都要在每个实例上重新创建一次。

  原型模式:创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。prototype其实就是通过调用构造函数而创建的那个实例对象的原型对象。
    function Person () {}
    Person.prototype.name = 'hello'
    Person.prototype.age = 12
    Person.prototype.job = 'web'
    Person.prototype.sayName = function () {
        console.log(this.name)
    }
    var p1 = new Person()
    var p2 = new Person()
    p1.sayName() // hello
    p2.sayName() // hello
    
    /**
        所有的方法和属性直接添加到Person的prototype属性中。新对象的这些属性和方法是有所有的实例共享的。
        1. 理解原生对象:只要创建了一个新的构造函数,就会根据一组特点的规则为该函数创建一个prototype属性,这个属性指向的是这个函数的原型对象。在默认情况下原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指针,指向prototype所在的函数,如上例:Person.prototype.constructor指向的是Person。
            a. isPrototypeOf方法
            创建了构造函数之后,调用构造函数创建一个新实例后,该实例包含一个__proto__(指的是:创建这个实例的构造函数的原型)属性,如上例:p1.__proto__ === Person.prototype。
            然而可以使用isPrototypeOf()方法来判断对象直接是否存在这种关系。
    */
    console.log(Person.prototype.isPrototype(p1)) // true
    console.log(Person.prototype.isPrototype(p2)) // true
    /**
            b. Object.getPrototypeOf方法:ES5新增的方法,返回一个对象。这个对象就是创建这个实例对象的构造函数的原型对象。
    */
    console.log(Object.getPrototypeOf(p1) === Person.prototype)
    // 还可以获取原型对象上的属性
    console.log(Object.getPrototypeOf(p1).name)
    
    /**
        2. 原型与in操作符:有两种方式使用in操作符;一种的单独使用,另一种是for-in。单独使用时,通过in操作符可以知道给定的属性是否能在对象上访问(只要能访问就行,不论是实例还是原型上)。然后可以通过hasOwnproperty方法来判断属性是否是在实例上。用上例:
    */
    console.log(p1.hasOwnProperty('name')) // false
    console.log('name' in p1) // true
    /**
        p1.name = 'lll'
        console.log(p1.hasOwnProperty('name')) // true
        console.log('name' in p1) // true
        
        在使用for-in时,返回的是所有能够通过对象访问、可枚举的属性,包括实例中的、原型中的。但是IE8及以下版本中例外,开发人员自定义的属性有可能是不可枚举的。如下:
        var o = {
            toString: function () {
                return 'on my o'
            }
        }
        for (var key in o) {
            if (key === 'toString') {
                console.log('toStrinig') // 在IE8及以下版本会不显示
            }
        }
    */
    // 正常的例子,如下:
    for (var inKey in p1) {
        console.log(inKey) // "name", "age", "job", "sayName"
    }
    /**
        a. 可以使用Object.keys()来获取对象的属性,不包括不可枚举和原型对象上的属性,如下
    */
    var p1Keys = Object.keys(p1)
    var keys = Object.keys(Person.prototype)
    console.log(p1Keys) // []
    console.log(keys) // ["name", "age", "job", "sayName"]
    /**
        b. 使用Object.getOwnpropertyNames()方法来获取对象的属性,不包括原型对象上的属性但是包括不可枚举属性,如下:
    */
    var ownKeys = Object.getOwnPropertyNames(Person.prototype)
    var p1OwnKeys = Object.getOwnPropertyNames(p1)
    console.log(ownKeys) // ["constructor", "name", "age", "job", "sayName"]
    console.log(p1OwnKeys) // []
    
    /** 3. 原型模式的缺点:如果原型包含引用类型的属性,会引起其共享所导致的问题。 */
  组合使用构造函数模式和原型模式:构造函数用于定义实例属性,原型属性用于定义方法和共享的属性。结果每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用。
    function Person (name, age, job) {
        this.name = name
        this.age = age
        this.job = job
        this.arr = ['a', 'b', 'c']
    }
    Person.prototype.sayName = function () {
        console.log(this.name)
    }
    var p1 = new Person('hello', 12, 'web')
    var p2 = new Person('work', 11, 'ui')
    p1.arr.push('d')
    console.log(p1.arr)
    console.log(p2.arr)
    console.log(p1.arr === p2.arr)
    console.log(p1.sayName === p2.sayName)

      这种构造函数和原型混成的模式,是目前ECMAScrpt中使用最广泛、认同度最高的一种创建自定义类型的方法。

  动态原型模式:可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。如下:
    function Person (name, age, job) {
        this.name = name
        this.age = age
        this.job = job
        if (typeof this.sayName != 'function') {
            Person.prototype.sayName = function () {
                console.log(name)
            }
        }
    }
    var p1 = new Person('hello', 12, 'web')
    p1.sayName() // hello
    
    /** 只有在sayName()不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用 */
  寄生构造函数模式:这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。这个函数又很想典型的构造函数。如下例:
    function Person(name, age, job) {
        var o = new Object()
        o.name = name
        o.age = age
        o.job = job
        o.sayName = function () {
            console.log(this.name)
        }
        return o
    }
    var p1 = new Person('hello', 12, 'web')
    p1.sayName() // hello
    /** 这里除了使用new操作符并把使用的包装函数叫构造函数之外,这个模式跟工厂模式其实一模一样。 */
    
    /** 这个模式可以在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用如下模式: */
    
    function Sper() {
        // 创建数组
        var arr = new Array()
        
        // 添加值
        arr.push.apply(arr, arguments)
        
        // 添加方法
        arr.toSper = function () {
            return this.json('|')
        }
        return arr
    }
    var p2 = new Sper('red', 'blue', 'green')
    console.log(p2.toSper())