美文网首页
JavaScript基础知识(二)

JavaScript基础知识(二)

作者: 做笔记的地方 | 来源:发表于2020-08-21 23:05 被阅读0次

    1面向对象的程序设计

    1.1 属性类型

    ECMAScript 中有两种属性:数据属性和访问器属性。

    1.1.1数据属性

    数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。

    • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。

    • [[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。

    • [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。

    • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined。

    var person = {}; Object.defineProperty(person, "name", {
    writable: false,
    value: "Nicholas" });
    alert(person.name); //"Nicholas" person.name = "Greg"; alert(person.name); //"Nicholas"
    

    这个例子创建了一个名为 name 的属性,它的值"Nicholas"是只读的。这个属性的值是不可修改 的,如果尝试为它指定新值,则在非严格模式下,赋值操作将被忽略;在严格模式下,赋值操作将会导 致抛出错误。
    把 configurable 设置为 false,表示不能从对象中删除属性。如果对这个属性调用 delete,则 在非严格模式下什么也不会发生,而在严格模式下会导致错误。而且,一旦把属性定义为不可配置的, 就不能再把它变回可配置了。此时,再调用 Object.defineProperty()方法修改除 writable 之外 的特性,都会导致错误。也就是说,可以多次调用 Object.defineProperty()方法修改同一个属性,但在把configurable 特性设置为 false 之后就会有限制了。在调用 Object.defineProperty()方法时,如果不指定,configurable、enumerable 和 writable 特性的默认值都是 false。

    1.1.2 访问器属性

    访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。 在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。

    • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特 性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为 true。
    • [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这 5 个特性的默认值为 true。
    • [[Get]]:在读取属性时调用的函数。默认值为 undefined。
    • [[Set]]:在写入属性时调用的函数。默认值为 undefined。
      访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。

    1.2 创建对象

    1.2.1工厂模式
    function createPerson(name, age, job){ var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
    alert(this.name); };
    return o; }
    

    函数 createPerson()能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式解决了创建 多个相似对象的问题.

    但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

    1.2.2构造函数模式
    function Person(name, age, job){ this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){ alert(this.name);
    }; }
    

    要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:
    (1) 创建一个新对象;
    (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
    (3) 执行构造函数中的代码(为这个新对象添加属性);
    (4) 返回新对象。
    构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个 实例上重新创建一遍。

    1.2.3原型模式
     function Person(){
        }
    Person.prototype.name = "Nicholas"; 
    Person.prototype.age = 29; 
    Person.prototype.job = "Software Engineer"; 
    Person.prototype.sayName = function(){
    alert(this.name); };
    
    1.2.4组合使用构造函数和原型模式
    function Person(name, age, job){
    this.name = name; 3 this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
    
    }
    
    Person.prototype = { constructor : Person, sayName : function(){
    
    alert(this.name); }
    
    }
    
    1.2.5寄生构造函数模式
    function Person(name, age, job){ var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
    alert(this.name); };
    return o; }
    
    1.2.6稳妥的构造函数模式
    function Person(name, age, job){
    //创建要返回的对象
    var o = new Object();
    //可以在这里定义私有变量和函数
    
    //添加方法
     o.sayName = function(){
    alert(name);
    }; 
    //返回对象
    return o; 
    }
    
    
    1.2.7继承

    可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用 instanceof 操作符,只要用 这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。

    alert(instance instanceof Object);   //true
    

    第二种方式是使用 isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该 原型链所派生的实例的原型,因此 isPrototypeOf()方法也会返回 true.

    alert(Object.prototype.isPrototypeOf(instance));  //true
    
    1.2.8 class类
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    }
    

    上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。

    Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

    class Point {
      // ...
    }
    
    typeof Point // "function"
    Point === Point.prototype.constructor // true
    

    上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
    constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
    类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    Foo.classMethod() // 'hello'
    
    var foo = new Foo();
    foo.classMethod()
    // TypeError: foo.classMethod is not a function
    

    注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

    class Foo {
      static bar() {
        this.baz();
      }
      static baz() {
        console.log('hello');
      }
      baz() {
        console.log('world');
      }
    }
    
    Foo.bar() // hello
    

    父类的静态方法,可以被子类继承。
    实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

    class foo {
      bar = 'hello';
      baz = 'world';
    
      constructor() {
        // ...
      }
    }
    

    上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。

    1.2.9 class继承

    Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

    class ColorPoint extends Point {
      constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
      }
    
      toString() {
        return this.color + ' ' + super.toString(); // 调用父类的toString()
      }
    }
    

    上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

    子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
    ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
    最后,父类的静态方法,也会被子类继承。

    2.函数表达式

    2.1 闭包

    闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
    当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。 然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境。 在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。
    作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。

    function createFunctions(){ 
    var result = new Array();
    for (var i=0; i < 10; i++){ 
    result[i] = function(){
    return i; };
    }
        return result;
    }
    

    这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返自己的索引值,即位置 0 的函数 返回 0,位置 1 的函数返回 1,以此类推。但实际上,每个函数都返回 10。因为每个函数的作用域链中 都保存着 createFunctions()函数的活动对象,所以它们引用的都是同一个变量 i。当 createFunctions()函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量 对象,所以在每个函数内部 i 的值都是 10。但是,我们可以通过创建另一个匿名函数强制让闭包的行为 9 符合预期,如下所示。

    function createFunctions(){ 
    var result = new Array();
    for (var i=0; i < 10; i++){
            result[i] = function(num){
                return function(){
                     return num;
                           }
               }(i)
           }
                    return result;
    }
    

    2.2 this

    在闭包中使用 this 对象也可能会导致一些问题。我们知道,this 对象是在运行时基于函数的执行环境绑定,在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等 于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。

    相关文章

      网友评论

          本文标题:JavaScript基础知识(二)

          本文链接:https://www.haomeiwen.com/subject/cdcmjktx.html