2020前端面试(十二)- 作用域与原型链

点这里,欢迎关注

一. 作用域相关

1.js的编译时与运行时

https://www.cnblogs.com/pianruijie/p/11454598.html

https://segmentfault.com/a/1190000018001871

https://segmentfault.com/a/1190000000533094

https://www.cnblogs.com/lulin1/p/9712311.html

http://www.frontopen.com/1702.html

js的执行总共有三个过程:语法分析 --> 预编译(词法分析) --> 解释执行

1.语法分析:

  • 这个阶段会检测js代码是否存在语法错误。
  • 还会构造出一颗抽象语法树(AST)

2.预编译阶段:(在进入执行环境时即进入到了预编译阶段)

  • 预编译分为两种:
    • 进入全局执行环境时的预编译:
      • 创建一个全局对象
      • 查找全局变量声明(包括隐式全局变量声明),变量名作全局对象的属性,值为undefined。
      • 查找函数声明,函数名作为全局对象的属性,值为函数引用。
    • 进入函数执行环境时的预编译:
      • 创建一个活动对象
      • 首先会为该活动对象初始化一个arguments属性,里面包含了实参的值。
      • 查找函数形参,添加在变量对象上,如果有对应的实参,则将实参的值赋给它,否则取值为undefined
      • 查找变量声明,将变量名作为变量对象的属性,值为undefined。
      • 查找函数声明,函数名作为变量对象的属性,值为函数引用
      • 然后会创建该执行环境中的作用域链,该作用域链是一个链表的结构,作用域链的前端,始终都是当前执行环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。作用域链中的下一个变量对象来自父级的执行环境,而再下一个变量对象则来自再下一个执行环境,一直延续到全局执行环境的变量对象;
    • 预编译阶段大部分会发生在函数执行前

3.解释执行阶段

2.执行环境(词法作用域)与作用域链

01.执行环境:

作用域模型有两种:词法作用域(静态作用域)动态作用域

词法作用域是指在词法阶段(预编译)定义的作用域,说它是静态的是因为代码在执行时,其词法作用域就已经确定了,词法分析器处理代码时其词法作用域是不变的。

词法作用域也称为执行环境,其定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象。环境中定义的所有变量和函数都保存在这个对象上。

02.全局执行环境:

全局执行环境是最外围的一个执行环境。根据 ECMAScript 实现所在的宿主环境不同,表示执行环
境的对象也不一样。在 Web 浏览器中,全局执行环境被认为是 window 对象。因此所有全局变量和函数都是作为window对象的属性和方法创建的。

03.作用域的分类:
**ES5:**只存在全局作用域和局部作用域(函数作用域)。局限:在if和for循环中声明的变量会变成全局变量;

ES5中通过立即执行函数可以模拟一个块级作用域,封装一些临时变量。

**ES6:**新增了块级作用域的概念,凡是{}包起来的都具有块级作用域,let,const声明的变量只能在块级作用域中使用。

有了块级作用域,就可以不用立即执行函数了。

04.作用域毁销毁

某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退
出——例如关闭网页或浏览器——时才会被销毁)。

05.执行流机制:

每个函数都有自己的执行环境(也就是有与之相关的变量对象)。当执行流进入一个函数时,会立马创建该函数的执行环境,该执行环境会被推入一个环境栈中。
**而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。**ECMAScript 程序中的执行流
正是由这个方便的机制控制着。

06.作用域链:

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。

作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问

作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;

全局执行环境的变量对象始终都是作用域链中的最后一个对象

07.变量的查找规则:

沿着作用域链一级一级向上查找。

08.作用域链与原型链的区别:

作用域链是针对变量的查找规则,原型链是针对构造函数的查找规则。

3.函数声明,函数表达式,匿名函数,立即执行函数:

https://www.cnblogs.com/lichunyan/p/7894867.html

//函数声明,存在函数提升
fn();
function fn(){
    console.log(111)
};
function (){
}()  //这是错误的语法,因为函数声明时必须带有函数名

//函数表达式
//函数表达式是作为表达式语句的一部分存在;当它没有函数名称的时侯,则称为匿名函数;
let fn2=function(){
    console.log(111)
}
fn2();

let fn3=function fn(){
    consploe.log(222)
}
fn3()

//只有函数表达式才能被执行符号()执行
+function test(){
    console.log(222)
}()


//里面的funciton会被解析为一个函数表达式,所以立即执行函数也被称为立即执行的函数表达式
(function(){})()

4.什么是闭包

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

闭包:

闭包是一种现象,由于存在作用域链的缘故,内部执行环境可以通过作用域链访问到外部执行环境的变量,闭包的产生是由于内部执行环境引用 了外部执行环境的变量,导致该变量在外部函数执行后其引用次数不为0,所以垃圾回收机制不会对该变量进行回收。所以可以一直访问并修改该变量。

5.立即执行函数IIFE

立即执行函数其实相当于立即执行的函数表达式。因为立即执行函数是由两个括号组合起来的,js引擎遇到第一个括号时会将其解析为函数表达式,然后遇到第二个括号时就会立即执行。

有什么用:
因为立即执行函数会开辟一个作用域**,**所有可以避免全局变量的污染。比如我们想让一个变量常驻在内存中,就可以在立即执行函数包裹某个函数,让这个函数引用立即执行函数中的变量,这样就会形成一个闭包,该变量不会被垃圾回收机制回收。

应用:

1.循环绑定事件时,为每一次循环绑定自己对应的索引值。(立即执行函数与闭包的结合,让立即执行函数内部的函数引用其变量)

for (var i = 0; i < btns.length; i++) {
    ((i) => {
        btns[i].onclick = function () {
            console.log(i);
        };
    })(i);
}

2.定时器,为每一个定时器函数绑定自己对应的索引值。(立即执行函数与闭包的结合)

for (var i = 0; i < 10; i++) {
    ((i) => {
        setTimeout(() => {
            console.log(i);
        }, 1000);
    })(i);
}

原理都是通过立即执行函数开辟新的作用域,让内部的函数引用立即函数的变量,形成闭包,从而可以保证每一个定时器函数引用的变量值都是不同的。

3.封装js库(开辟一个新的作用域,防止污染全局变量)

应用1,2属于立即执行函数与闭包的结合应用,应用3应用的是立即执行函数会开辟一个新的作用域的特性。

6.闭包有什么作用

  • 保护函数中变量的安全性。
    • 由于外部函数在执行后会被销毁,此时就只剩下了被内部函数引用的变量,因此该变量只能通过引用他的函数访问到。
    • 利用这个特性可以实现私有属性和私有方法(见应用5)。(即只能通过特定的方式调用)
  • 可以在内存中维持一个变量,利用这个特性可以实现单例模式,防抖和节流,once函数。

7.闭包在实际中的应用:

(1)定时器
(2)事件监听器
(3)单例模式
(4)js的节流和防抖

https://www.cnblogs.com/momo798/p/9177767.html

在进行窗口的resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果。

函数的防抖和节流是闭包的充分应用。

函数防抖(debounce):

当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

// 防抖
function debouce(fn, wait) {
    let timeout = null;
    return function () {
        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(fn, wait);
    };
}
//   处理函数
function handle() {
    console.log(Math.random());
}
//   滚动事件
window.addEventListener("scroll", debouce(handle, 1000));

函数节流(throttle):

当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

方法一:时间戳

function throttle(fn, delay) {
    let prev = Date.now();
    console.log(prev);
    return function () {
        let now = Date.now();
        if (now - prev > delay) {
            fn();
            prev = Date.now();
        }
    };
}
function handle() {
    console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000));

方法二:定时器

function throttle(fn, delay) {
    let timer = null;
    return function () {
        if (!timer) {
            timer = setTimeout(function () {
                fn();
                timer = null;
            }, delay);
        }
    };
}
function handle() {
    console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000));

方法三:setTimeout+requestAnimationFrame(看看就好)

//requestAnimationFrame可以保证每次重绘最多调用一次回调函数。因此可以用来实现节流。
let enabled = true;
function fn() {
    console.log(Math.random());
}
window.addEventListener("scroll", () => {
    //设置标记变量,可以过滤掉多余requestAnimationFrame的调用
    if (enabled) {
        enabled = false;
        window.requestAnimationFrame(fn);
        //手动限制操作执行的频率
        window.setTimeout(() => {
            enabled = true;
        }, 1000);
    }
});

节流与防抖的区别:

函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。

比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

(5)如何实现一个私有变量,用getName方法可以访问,不能直接访问

利用闭包来实现:

function createPerson(name) {
    var name = name;
    function Person() {}
    Person.prototype.getName = function () {
        return name;
    };
    Person.prototype.setName = function (value) {
        name = value;
    };
    return Person;
}
let Person = createPerson("liu");
let person = new Person();
console.log(person.name); //undefined
console.log(person.getName()); //liu
person.setName("haha");
console.log(person.getName()); //haha

let Person2 = createPerson("liu1");
let person2 = new Person2();
console.log(person2.getName()); //liu1
(6)实现一个once函数(传入函数参数),使其只能执行一次

利用闭包来解决,在函数的开始设置一个标记值,当执行完传入的函数后,更改这个标记值。

function sing(arg1, arg2) {
    console.log(`i like the singer ${arg1} and ${arg2}`);
}
function once(func) {
    let b = true;
    return function () {
        if (b) {
            func.apply(null, arguments);
            b = false;
        } else {
            console.log(undefined);
        }
    };
}
// sing("adele", "wangfei");
let fn = once(sing);
fn("adele", "wangfei"); //i like the singer adele and wangfei
fn("adele", "wangfei"); //undefined
fn("adele", "wangfei"); //undefined

该函数与防抖和节流原理相同,都是利用闭包来实现。

二.JS中的垃圾回收机制

js内存的分配以及无用内存的回收都实现了自动管理。垃圾回收机制的原理是找出不再继续使用的变量,然后释放其占用的内存。垃圾收集器会按照固定的时间间隔周期性地执行这一操作。

垃圾回收方式有两种:

标记清除:

这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。

垃圾回收器在‘运行的时候’会给存储在内存中的所有变量都加上标记,然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),然后再删除所有被标记的变量,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。

引用计数法:

另一种不太常见的方法就是引用计数法,引用计数法的含义就是跟踪记录每个值被引用的次数。

当声明了一个变量,并用一个引用类型的值赋值给该变量,则这个值的引用次数为1。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

var arr=[1,2,3,4];
arr=null;//解除arr对[1,2,3,4]引用,这块内存就可以被垃圾回收机制释放了。

引用计数法存在一个问题:循环引用。

function problem() {
    var objA = new Object();
    var objB = new Object();
    objA.someOtherObject = objB;
    objB.anotherObject = objA;
}

循环引用导致的问题就是当函数执行完毕后,这两个对象的计数均不为0,如果大量存在这种相互引用,就会导致内存泄漏

三. 原型链相关

1.讲一讲js原型链,原型链的顶端是什么?Object的原型是什么?Object的原型的原型是什么?

原型对象,对象原型,实例对象的区别:

构造函数默认带有一个prototype属性,这个的属性值是一个对象,即原型对象

原型对象上有一个constructor属性,指向了这个构造函数。

由构造函数创建的实例对象身上有一个__proto__属性,这个属性指向了构造函数的原型对象,这个属性也被称为对象原型

原型链

实例对象的__proto__指向了构造函数的原型对象;

构造函数的原型对象的__proto__指向了Object的原型对象;

Object的原型对象的__proto__指向为null;

所以,原型链顶端是Object构造函数的原型对象。

对象身上属性和方法的查找规则

依据原型链逐层向上查找。

利用原型链实现继承:

ES6之前JS没有类和继承的概念,但可以通过原型来实现继承。

让子构造函数的原型对象指向父构造函数的实例对象,这样当子构造函数不存在某个方法时,就会去原型对象上找,也即父实例对象,然后再找不到则会去父构造函数的原型对象上找。

注意1:之所以这里子构造函数的原型对象不直接指向父构造函数,是因为如果改变该原型对象,实则更改的是父构造函数的原型对象。

注意2:将原型对象指向父构造函数的实例对象后,原来原型对象上的constructor属性就被覆盖了,因此需要单独为其添加constructor属性,并指向子构造函数。

2.在数组原型链上实现删除数组重复数据的方法:

直接在原数组构造函数的原型对象上添加去重函数即可:

Array.prototype.cut=function(){
    let obj={}
    for(let i=0;i<this.length;i++){
        let tmp=this[i];
        if(obj[tmp]){
            this.splice(i,1);
            i--;
        }else{
            obj[tmp]=tmp;
        }
    }
    obj=null;
    return this;
}
let arr=[1,2,3,4,3,4,5,6,1];
let newArr=arr.cut();
console.log(newArr)//[1, 2, 3, 4, 5, 6]



Array.prototype.cut=function(){
    return [...new Set(this)];
}
let newArr=[11,22,33,44,55,22,33].cut();
console.log(newArr);//[11, 22, 33, 44, 55]

3.js的new操作符做了哪些事情:

https://blog.csdn.net/lxcao/article/details/52792466

共四步:

1、创建一个空对象 obj

2、设置原型链 obj.__proto__=Func.prototype

3、调用 Func.apply(obj,arguments),让Func中的this指向创建的这个对象,并执行Func的函数体。

4、判断Func的返回值类型。(默认是返回前面创建的对象obj)

如果是值类型,返回原obj。如果是引用类型,就返回这个引用类型的对象。

4.实现一个new :

//这里体现了工厂模式的设计思想,ObjectFactory相当于一个工厂,设计了一个统一的接口,会根据参数生成对应的实例对象。
function Person(name, age) {
    this.name = name;
    this.age = age;
}
function ObjectFactory() {
    // 创建一个空对象  obj
    var obj = {};

    var constructor = Array.prototype.shift.call(arguments);
    // 设置原型链   obj.__proto__=Func.prototype
    obj.__proto__ = constructor.prototype;
    //   调用 Func.apply(obj,arguments),让Func中的this指向创建的这个对象,并执行Func的函数体。
    var result = constructor.apply(obj, arguments);
    // 判断Func的返回值类型。如果是值类型,返回原obj。如果是引用类型,就返回这个引用类型的对象。
    return typeof result === "object" ? result : obj;
}
var person = ObjectFactory(Person, "liu", 21);
console.log(person); //Person { name: 'liu', age: 21 }

5.this的指向问题:

this的7种绑定:

1.普通函数 window

2.对象的方法 对象

3.构造函数 实例对象

4.事件处理函数 事件源

5.定时器函数 window

6.立即执行函数 window

7.箭头函数(神仙棒,搅屎棍,可能会改变以上所以情况中的this指向,所以在使用箭头函数的时候,需要擦亮双眼看清this指向的到底是谁)

箭头函数中的this是静态的,也就是作用域是不会被改变的,始终指向的是该箭头函数声明时所在的真正的执行环境

//实例1:
let o = {
    fn: function () {
        setTimeout(() => {
            console.log(this);
        });
    },
};
o.fn();//{fn: ƒ}

//实例2:
function fn2() {
    setTimeout(() => {
        console.log(this);
    });
}
fn2();//window

//实例3:
window.name = "liu1";
let func5 = () => {
    console.log(this.name);
};
// 箭头函数中this的指向不会改变
func5.call(obj); //liu1



//实例4:
var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo,100);//0

//实例4等价于:
var a = 0;
setTimeout(function foo(){
    console.log(this.a);
},100);//0

6.改变函数内部this指向的方法:

callapplybind
是否会立即执行函数不会,但会返回一个新的函数
传递的第二个参数arg1,arg2的形式数组的形式arg1,arg2的形式
实际应用场景实现属性的继承常跟数组有关系,可以借助Math对象查找数组的最大值最小值应用1:定时器函数中默认的this为window,可以通过bind指定其中的this,好处是不会立即调用该函数
应用2:适用于任何想要改变this指向,但不想立即执行的情况,比如绑定事件处理函数时,可以通过bind改变this的指向。

7.实现一个call函数:

//原理:原函数中this的指向是由调用函数的上下文环境决定的,如果将上下文环境改为目标对象,则最终函数内部this指向的就是该目标对象
//简单原理:
function fn1() {
    console.log(this.name);
}
var name = "liu";
var obj = {
    name: "liu2",
};
obj.fn = fn1;
obj.fn(); //liu2
//具体实现:
function fn1() {
    console.log(this.name);
    console.log("参数为", ...arguments);
}
var name = "liu";
var obj = {
    name: "liu2",
};
Function.prototype.my_call = function (obj) {
    //细节点1,判断是否传入第一个参数
    obj = obj || window;
    //核心:将原函数挂载在目标对象的一个属性上,通过该对象.属性调用该函数时函数内部的指向的就是该目标对象。
    obj.fn1 = this;
    //var rs = obj.fn1(...[...arguments].slice(1));
    var str = "obj.fn(";
    for (var i = 1; i < arguments.length; i++) {
        str += arguments[i] + ",";
    }
    str = str.substring(0, str.length - 1);
    str += ")";
    var rs = eval(str);
    //细节点2:删除添加在原对象上的函数属性,不留痕迹,彻彻底底。
    delete obj.fn1;
    return rs;
};
fn1.my_call(obj, 1, 2, 3); //liu2 参数为 1, 2, 3
fn1.my_call(); //liu 参数为

8.实现一个apply函数:

//原理与实现call函数类似,都是通过改变原函数的调用环境来实现。
function fn() {
    console.log(this.name);
    console.log("传入的参数为", ...arguments);
}
var name = "liu";
var obj = {
    name: "xiao",
};
// fn.apply(obj, [1, 2, 3]);//xiao
Function.prototype.my_apply = function (obj, args) {
    obj = obj || window;
    //核心与call方法的实现一致
    obj.fn = this;
    //细节:需要根据第二个参数来决定函数的调用方式
    if (args && args.length) {
        var str = "obj.fn(";
        for (var i = 0; i < args.length; i++) {
            str += args[i] + ",";
        }
        str = str.substring(0, str.length - 1);
        str += ")";
        var rs = eval(str);
    } else {
        var rs = obj.fn();
    }
    delete obj.fn;
    return rs;
};
fn.my_apply(obj, [1, 2, 3]);
fn.my_apply(obj);

9.实现一个bind函数:

function fn() {
    console.log(this.name);
    console.log("传入的参数为", ...arguments);
}
var name = "liu";
var obj = {
    name: "liu2",
};
// var newFn = fn.bind(obj, 1, 2, 3, 4);
// newFn(); //liu2
Function.prototype.my_bind = function (obj) {
    obj = obj || window;
    //核心1:将原函数绑定在目标对象的属性上
    obj.fn = this;
    const args = arguments;
    //核心2;利用闭包返回一个函数
    return function () {
        var str = "obj.fn(";
        for (var i = 1; i < args.length; i++) {
            str += args[i] + ",";
        }
        str = str.substring(0, str.length - 1);
        str += ")";
        var rs = eval(str);
        return rs;
    };
};
var newFn = fn.my_bind(obj, 1, 2, 3, 4);
newFn(); //liu2 传入的参数为 1 2 3 4

10.Function.__proto__是什么?

表示Function构造函数的原型对象,指向的是Object构造函数的原型对象

Object.getPrototypeOf:用于获取对象的原型对象,可以用来代替__proto__;因为__proto__中的下划线可能存在语义性的问题,该属性可能还存在兼容性的问题。

let object = {};
console.log(Object.getPrototypeOf(object) === object.__proto__); //true

11.js实现继承的几种方式:

function Animal(name, size, likes) {
    this.name = name || "Animal";
    this.size = size || "small";
    this.sleep = function () {
        console.log(this.name + " is sleeping");
    };
    this.likes = likes || [];
}
Animal.prototype.eat=function(food){
    console.log(this.name+'is eating '+food);
}

① 原型链继承

//核心: 将父类的实例作为子类的原型
//优点:简单,容易实现
//缺点:无法实现多继承,无法向父类构造函数传参;原型对象上的所有属性被所有实例共享,只有一份
function Cat() {}
Cat.prototype = new Animal();  //核心代码
Cat.prototype.name = "tom";
var cat = new Cat();
console.log(cat.name);
cat.sleep();
console.log(cat instanceof Cat); //true 
console.log(cat instanceof Animal); //true    即子类的实例,也是父类的实例

var cat2 = new Cat();
cat.likes.push("play");
console.log(cat2.likes); //[ 'play' ]

② 构造继承

//优点:可以向父类传递参数,可以实现多继承(call多个父类实例)

//缺点:
//实例并不是父类的实例,只是子类的实例
//只能继承父类的实例属性和方法,不能继承原型属性/方法
function Cat(name) {
    Animal.call(this,name); //核心代码
}
var cat = new Cat("tom");
console.log(cat.name); //tom
cat.sleep(); //tom is sleeping

③ 实例继承

// 核心:在子构造函数中创建一个父类实例,可以在上面添加子类的新特性,然后返回这个对象
// 特点:可以通过new调用,也可直接调用
// 缺点:实例是父类的实例,不是子类的实例; 不支持多继承; 而且在子构造函数的原型对象上添加方法是无效的,根据new关键字的执行过程可知。
function Cat(name) {
  var instance = new Animal();      //核心代码
  instance.name = name || "Tom";
  return instance;
}
Cat.prototype.sing = function () {
  console.log("sing a song");
};
var cat = new Cat("jim");
console.log(cat.name); //jim
cat.sleep(); //jim is sleeping
cat.sing(); // cat.sing is not a function

④ 拷贝继承:

// 利用for..in可以遍历对象的原型对象上的属性的特性
// 特点:可以多继承
// 缺点:因为存在拷贝,内存占用高;无法获取父类不可枚举的方法(for in 只能获取枚举的属性)
function Cat(name) {
    var animal = new Animal();
    for (key in animal) {
        Cat.prototype[key] = animal[key];    //核心代码
    }
    Cat.prototype.name = name || "tom";
}
var cat = new Cat("jim");
console.log(cat.name);
cat.sleep();
console.log(cat instanceof Animal); //false
console.log(cat instanceof Cat); //true

⑤ 组合继承:

// 原型链继承+构造继承
// 优点:
// 弥补了原型链继承的局限性,可以传参;不存在引用属性共享的问题,因为代码1将实例属性绑定在了实例对象上,不需要再去原型对象上找属性了。
// 弥补了构造继承的局限性,可以继承原型上的属性和方法,既是子类的实例,也是父类的实例。
// 缺点:初始化了两次实例方法/属性,多消耗了一些内存
function Cat(name, size) {
    Animal.call(this, name, size);     //核心代码1
}
Cat.prototype = new Animal();     //核心代码2
var cat = new Cat("alice", "big");
console.log(cat.name); //alice
console.log(cat.size); //big
cat.sleep(); //alice is sleeping

⑥ 寄生组合继承(⭐️ ):

// 解决组合继承中的冗余问题,将 实例属性/方法的继承 与 原型对象上属性/方法的继承 分开
function Cat(name, size) {
    Animal.call(this, name, size); //核心代码1:继承实例属性和方法
    (function () {
        var Super = function () {};
        Super.prototype = Animal.prototype; //核心代码2:通过寄生方式,砍掉父类的实例方法/属性,这样就不会初始化两次实例方法/属性,避免的组合继承的缺点
        Cat.prototype = new Super();
    })();
}
var cat = new Cat("alice", "big");
console.log(cat.name); //alice
console.log(cat.size); //big
cat.sleep(); //alice is sleeping

⑦ ES6中的class:

class Animal {
    constructor(name, size) {
        this.name = name;
        this.size = size;
    }
    sing() {
        console.log("sing a song");
    }
}
class Cat extends Animal {
    constructor(name, size) {
        super(name, size);
    }
    eat() {
        console.log("i like eating mouse");
    }
}
var cat = new Cat("tom", "small");
console.log(cat); //Cat { name: 'tom', size: 'small' }
cat.sing(); //sing a song
cat.eat();

``
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值