每日一题: JS

200 阅读10分钟

1. 执行上下文

我们知道,每当函数调用时都会创建新的执行上下文,这也就是说,执行上下文在代码执行时才确定。

  • 在创建执行上下文这个过程中做了以下几件事:
    • 创建变量对象(变量对象中保存着变量、函数声明以及形式参数(arguments),变量对象在函数执行时变成可访问的活动对象);
    • 创建作用域链;
    • 确定this指向;
  • 它的类型:
    • 全局执行上下文;
    • 函数执行上下文;
    • eval执行上下文;
  • 代码执行过程:
    • 创建全局上下文 (global EC);
    • 全局执行上下文 (caller)逐行自上而下执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层;
    • 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起 函数执行完后,callee 被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行;

2. 作用域(词法环境)

作用域在定义时就已经确定,区别于执行上下文。作用域决定了代码区块中变量和其他资源的可见性。

  • 全局作用域;
  • 函数作用域;
  • 块级作用域(ES6新增let,const);

3. 作用域链

在代码执行时,会创建变量对象的一个作用域链,用途:保证对执行环境有权访问的所有变量和函数的有序访问

作用域链的最前端,始终是当前执行的代码所在的执行上下文的变量对象,如果是函数执行上下文,则将其活动对象作为变量对象,全局执行上下文的变量对象始终都是作用域链的最后一个对象。搜索变量和函数名时,如果搜索不到则再搜索上一级作用域链。

4. 闭包

闭包是指能够访问另一个函数作用域中变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数。

var a = 10
function getValue() {
    let a = 20
    return function() {
        console.log(a)
    }
}
getValue()() // 20

当外部函数执行,内部匿名函数被返回后,它的作用域链被初始化为包含外部函数的活动对象和全局变量对象,这样匿名函数就可以访问外部函数中的变量。更为重要的是,当外部函数执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个对象。

5. this指向

this总是指向最后调用它的那个对象,这句话不知道是否严谨,但至少打通了我对this的认识,请看this详解

this对象是在运行时基于函数的执行上下文绑定的:

  • 在全局函数中,this指向Window;
  • 当函数作为某个对象的方法调用时,this指向那个对象;
  • 匿名函数的执行上下文具有全局性,因此匿名函数中的this通常指向Window;

改变this指向:

  • getValue.call(Obj,val1,val2)
  • getValue.apply(Obj, [val1, val2])
  • getValue.bind(Obj)(val1, val2)
  • 箭头函数
  • new关键字

很显然,我们能看出这几个方法的区别:

  • callapply在改变this指向的同时立即执行函数,而bind并不立即执行;
  • callapply传递参数也有区别,call接收多个参数,apply接收一个数组

6. 原型和原型链

原型是一个对象(prototype),用于实现对象的继承,在火狐和谷歌浏览器中,对象都有一个__proto__属性指向该对象的原型对象。

原型链是由原型对象组成,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针,那么当原型对象等于另一个类型的实例时,此时的原型对象将包含一个指向另一个原型的指针,如此便形成了原型的链条,这就是原型链的基本概念。

  • 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出undefined;
  • 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。

7. 继承

  • 原型链继承:

    特点:

    • 子类的原型是父类的实例
    • 父类新增原型属性和原型方法子类都能访问到

    问题:

    • 包含引用类型值的原型属性会被所有实例共享
    • 无法实现多继承
    • 创建子类的实例时,不能像父类型的构造函数中传参
    //父类
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    Person.prototype.getHobby = function() {
        return this.hobby;
    }
    // 子类
    function Student(hobby) {
        this.hobby = hobby;
    }
    // 子类的原型是父类的实例
    Student.prototype = new Person('zjx', 18);
    var student1 = new Student('LOL');
    console.log(student1.getHobby()) // LOL
    
  • 构造函数继承

    特点:

    • 解决子类实例共享父类引用属性的问题
    • 可实现多继承
    • 可以在子类的构造函数中向父类构造函数传递参数

    问题:

    • 方法都在构造函数中定义,因此函数复用无从谈起
    • 父类的原型中定义的方法在子类中无法访问
    function Person(name) {
        this.name = name
    }
    function Student(val) {
        Person.call(this, val)
    }
    // 传参
    var student1 = new Student('zjx')
    console.log(student1.name) // zjx
    
  • 组合继承

    • 这种继承方式避免了以上两种的缺陷,融合了他们的优点,成为最常用的继承模式

    缺点:

    • 调用了两次父类构造函数
    // 父类
    function Person(name) {
        this.name = name
    }
    Person.prototype.getName = function() {
        return this.name
    }
    // 子类
    function Student(name, age) {
        Person.call(this, name) // 第2次
        this.age = age
    }
    // 子类的原型是父类的实例
    Student.prototype = new Person() // 第1次
    // 因为上一步修改了原型,所以constructor也被修改,这一步将constructor指回子类
    Student.prototype.constructor = Student
    var student1 = new Student('zjx', 18)
    console.log(student1.name, student1.age, student1.getName()) //  'zjx' 18 'zjx'
    
  • 原型式继承

    // 用对象person作为原型对象
    var person = {
        name: 'zjx',
        hobby: ['pingpong', 'basketball']
    }
    var student1 = Object.create(person, {
        name: {
            value: 'zy'
        }
    })
    var student2 = Object.create(person, {
        name: {
            value: 'hml'
        }
    })
    student2.hobby.push('LOL')
    // 这里看出问题了吧,包含引用类型的属性始终都会共享相应的值
    console.log(student1.hobby, student2.hobby) // ['pingpong', 'basketball', 'LOL'] ['pingpong', 'basketball', 'LOL']
    
  • 寄生式继承 寄生式继承的思路与工厂模式类似,即创建一个仅用于封装继承过程的函数,由于不能做到函数复用而降低效率,这一点与构造函数模式类似。

    function createAnthor(obj) {
        var newObj = Object.create(obj); // Object.create()并不是必须的,任何可以返回对象的函数都适用于此模式
        newObj.sayHi = function() { // 在这里增强对象
            console.log('hi');
        }
        return newObj; // 返回这个对象
    }
    var person = {
        name: 'zjx',
        age: 18
    }
    var anthorPerson = createAnthor(person);
    anthorPerson.sayHi(); // 'hi'
    
  • 寄生组合式继承

    为了解决组合式继承调用了两次父类构造函数而存在的继承模式

    function Person(name) {
        this.name = name;
    }
    Person.prototype.getName = function() {
        return this.name;
    }
    function Student(name, age) {
        Person.call(this, name); // 只在这里调用了一次父类构造函数
        this.age = age;
    }
    // 继承原型
    function inheritPrototype(subType, superType) {
        let prototype = object.create(superType.prototype); // 1.创建超类原型的副本
        prototype.constructor = subType; // 2.为副本添加constructor属性
        subType.prototype = prototype; // 3.将新对象赋给子类的原型
    }
    inheritPrototype(Student, Student);
    Student.prototype.sayAge = function() {
        return this.age
    }
    

8. 创建对象

查看详情 虽然Object构造函数和对象字面量都看可以用来创建单个对象,但这些方式有明显的缺点:使用同一接口创建很多对象,会产生大量的重复代码,因此便又衍生出了下面这些创建对象的模式。

  1. 工厂模式

    优点:

    • 解决了创建多个相似对象的问题

    缺点:

    • 却没有解决对象的识别问题
    function createPerson(name, age) {
        var o = new Object();
        o.name = name;
        o.age = age;
        o.sayName = function () {
            return this.name;
        }
        return o;
    }
    var person1 = createPrson('zjx', 18)
    
  2. 构造模式

    优点:

    • 解决了对象的识别问题

    缺点:

    • 每个实例都会创建不同的function实例,而其实创建完成同样任务的function实例是很没有必要的
    function Person(name, age) {
        this.name = name
        this.age = age
        this.sayName = function() {
            return this.name
        }
    }
    var person1 = new Person('zjx', 18)
    
  3. 原型模式

    优点:

    • 不用为构造函数传递参数,可以创建多个相同的对象

    缺点:

    • 原型中的属性被很多实例共享,当属性为包含引用类型值的属性时,修改一个实例中属性的值,另一个实例中的属性的值也会改变
    function Person() {
    }
    Person.prototype.name = 'zjx';
    Person.prototype.age = 18;
    Person.prototype.sayName = function() {
        return tihs.name;
    }
    var person = new Person();
    
    
  4. 组合模式 优点:

    • 集构造模式与原型模式之所长,也是当前使用最广泛的模式
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    Person.prototype.sayName = function() {
        return this.name;
    }
    var person1 = new Person('zjx', 18);
    
  5. 动态原型模式

    优点:

    • 这种模式将所有信息都封装在了构造函数里,因为在组合构造函数模式和原型模式中,构造函数和原型模式是独立的,通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点,换句话说,就是可以通过在构造函数中,检查某个应该存在的方法是否有效,来决定是否需要初始化原型
    funtion Person(name, age) {
        this.name = name;
        this.age = age;
        if(typeof this.sayName != 'function') { // 判断是否存在该方法来决定是否需要初始化原型
            Person.prototype.sayName = function() {
                return this.name;
            }
        }
    }
    var person1 = new Person('zjx', 18);
    
  6. 寄生构造函数模式

    这种模式其实和工厂模式很像,除了使用new操作符并把使用的包装函数叫做构造函数之外,和工厂模式可以说是一模一样的,这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

    那么既然有了工厂模式,为什么还要有寄生构造函数模式呢?其实这个模式主要是用来给js原生的构造函数定义一些新的方法,我们可以看下面这个例子:

    function SpecialArray() {
        var values = new Array();
        values.push.apply(values, argumens);
        values.toPipedString = function() {
            return this.join("|");
        }
        return values;
    }
    
    var colors = new SpecialArray("red","blue");
    console.log(colors.toPipedString());  //  red|blue
    
  7. 稳妥构造模式

    • 新创建对象的实例方法不应用this;不适用new操作符调用构造函数;实例中的属性除了实例方法可以访问以外,没有其他的办法访问实例属性的值。
    function Person(name, age) {
    var o = new Object();
        _name = name;
        _age = age;
        o.sayName = function() {
            return _name;
        }
        return o;
    }
    
    var person1 = Person("zjx", 29);
    person1.sayName();  //  Nicholas
    
  8. ES6 Class

9. new运算符执行过程

  1. 创建一个空对象;
  2. 将新对象的原型指向构造函数的原型;
  3. 绑定this为当前对象;
  4. 返回这个新对象(如果构造函数有自己的返回值,且该返回值为对象时,则返回这个值;如果返回值不为对象,则返回刚创建的这个新对象);

10. 对象拷贝

  • 浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响
  • 具有局限性的深拷贝:在没有耳机属性的情况下,一下这两种方法属于深拷贝,在对象层级较深的情况下,他们依然只能做浅拷贝
    • Object.assign
    • 展开运算符(...)
  • 深拷贝: 完全拷贝一个新对象,修改时原对象不再受到任何影响
    • JSON.parse(JSON.stringify(obj))
      • 具有循环引用的对象时,报错
      • 当值为函数、undefined、或symbol时,无法拷贝
    • 递归
    function deepClone (target) {
        let result;
        if (typeof target === 'object') {
            if (Array.isArray(target)) {
                result = []
                for (let i in target) {
                    result[i] = deepClone(target[i])
                }
            } else if (target === null) {
                result = null
            } else if (target.constructor === RegExp) {
                result = target;
            } else {
                result = {}
                for (const i in target) {
                    result[i] = deepClone(target[i])
                }
            }
        } else {
            result = target
        }
        return result
    }
    

最后要说一句,在实际生产环境中,我们还是要借助lodash的deepClone或者其他已封装好的方法,上面我们所写的方法还有很多没有考虑到的地方,比如ArrayBuffer什么得我们都没有进行处理。