4.Javascript高级

参考:

https://github.com/mqyqingfeng/Blog深入系列

https://www.bilibili.com/video/BV14s411E7qf

原型与原型链

1.理解

1.每个函数都有2条属性,显式原型属性prototype和隐式原型属性—proto—

2.函数的实例对象f1,f2,01,02函数的原型对象Foo.prototype,Function.prototype(new出来的object对象)都只有隐式原型属性—proto—,它们的—proto—指向new它们的函数的原型对象。即:对象都有—proto—

3.值得一提的是Object.prototype不是new出来的,它是最初始的,object.prototype里面保存的是String等方法。object.prototype.-proto-指向的是null,object.prototype是原型链的尽头.无法再往上查找了。

//instanceof表示前者能不能顺着__proto__找到后者的prototype
console.log(Foo.prototype instanceof Object) //true
console.log(Fuction.prototype instanceof Object) //true
console.log(Object.prototype instanceof Object) //false

4.所有函数:Object(),Function(),function Foo(),的**-proto-指向的都是new它们的Function函数的原型对象**,Function是由自己产生自己的,所以Function.prototype===Function.—proto—

5.constructor:构造器

位置:存在于构造函数的prototype上,系统为我们自己添加的

作用:让实例对象能够通过—proto—.consructor找到它的构造函数

4.Javascript高级_第1张图片

2.原型修改/重写

1.如果使用Person.prototype.getName=function或者Person.prototype.a=3这样的方式,系统会帮我们创建Person的原型对象,然后执行Person.prototype.constructor=Person

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('aa')
console.log(p.constructor===Person) //true
// p没有constructor,会去p.__proto__上找constructor

2.如果使用Person.prototype={}的方式创建,会先执行{}=new Object(), Obeject.prototype.constructor=Object,然后把{}的地址赋给Person.prototype

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype = {
   sayename: function() {}
}
var p = new Person('aa')
//p没有constructor
console.log(p.hasOwnProperty('constructor')) //false

//通过p.__proto__找到{sayname:f},也没有constructor
console.log(p.__proto__.hasOwnProperty('constructor')) //false

//通过p.__proto__.__proto__找到constructor。它指向Object
console.log(p.__proto__.__proto__.hasOwnProperty('constructor'))//true
console.log(p.constructor===Object) //true

//如果想p.constructor指向Person
//p.constructor=Person
//console.log(p.constructor===Person) //true

3.原型的使用

我们一般将构造函数的方法保存在原型链,属性保存在实例对象。因为方法是通用的,每个对象会有不同的属性。

tips:一个小坑

对于下列代码期待输出的是实例对象 fn1{test1:f} 结果是:Fn{test1:f}

function Fn(name){
    this.test1=function(){
        console.log('test1')
    }       
    console.log(this)
}
var fn1 = new Fn() //Fn{test1:f}

原因:控制台中使用Fn{test1:f}来表示实例对象,使用函数来表示构造函数

4.Javascript高级_第2张图片

4.面试题

1.

function A(){
}
A.prototype.n=1
var b=new A()
A.prototype={	//这里改变了地址,指向了另一个对象
    n:2,
    m:3
}  
var c=new A()
console.log(b.n,b.m,c.n,c.m)//1 undifine 2 3

2.

function F(){

}
Object.prototype.a=function(){
    console.log('a()')
}
Function.prototype.b=function(){
    console.log('b()')
}

var f=new F()
F.a()  //a()   
F.b()   //b()
f.a()   //a()
f.b()   //f.b is not a function

//F是Function new出来的,Function.prototype是Object new出来的
//f是F new出来的,F.prototype是Object new出来的

判断总结:

1.对于对象(对象只有—proto—):

1.原型对象:原型对象的—proto—都指向Object.prototype

2.实例对象:谁new它就指向谁的prototype

2.对于函数(函数有—proto—和prototype)

1.函数的—proto—:都指向Function.prototype,包括Function本身

2**.函数的prototype**:都是通过Object new出来的原型对象

执行上下文/作用域链/闭包/参数传递

1.执行上下文

0.每个执行上下文都要三个重要的属性

1.变量对象(Variable object,VO)

全局上下文中,变量对象就是window

函数上下文中,用活动对象(activation object, AO)来表示变量对象。活动对象是在进入函数上下文时刻才被创建的,所以成为活动对象。

变量对象会包括:

1.函数的所有形参 (如果是函数上下文)

  • 由名称和对应值组成的一个变量对象的属性被创建
  • 没有实参,属性值设为 undefined

2.函数声明

  • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
  • 如果变量对象已经存在相同名称的属性,则完全替换这个属性

3.变量声明

  • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
  • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

例如:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}
foo(1);

在进入执行上下文后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

还是上面的例子,当代码执行完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

2.作用域链(Scope chain)
3.this

1.全局执行上下文

1.在执行全局代码前会创建全局执行上下文,压入栈,

2.然后先将变量提升赋为undefine

3.将函数添加到window上

4.将this绑定为window

5.逐步执行全局代码

2.函数执行上下文

1.全局代码在执行到函数时,会添加一个函数执行上下文,将上下文压入栈

2.将参数赋值给实参,添加为执行上下文的属性

3.将参数添加给argument,添加argument为执行上下文的属性

4.变量提升,添加为执行上下文的属性

5.将函数里的函数添加为执行上下文里的方法

6.this绑定为调用函数的对象,

7.逐步执行函数体代码

3.执行上下文栈

1.因为函数执行上下文是调用函数就进栈,执行完就释放该区域的栈空间,所以函数里的变量是不能保存的

4.面试题

1.
console.log('globle begin '+i) 
var i=1							
foo(1);							 
function foo(i){					
    if(i==4){							
        return								
    }										
    console.log('foo()begin'+i)				
    foo(i+1)
    console.log('foo() end'+i)
}
console.log('globle end'+i)
//输出结果:
//globle begin undefined
//foo()begin1
//foo()begin2
//foo()begin3
//foo() end3
//foo() end2
//foo() end1
//globle end1
2.
function a(){
}
var a;
console.log(typeof a)//function
//挂载函数->变量提升(重名不干扰),重名的话->绑定this->运行代码

if(!(b in window)){
    var b=1
}
console.log(b)//undefined
//没有调用函数就没有新的执行上下文,
//var会在最开始就变量提升

var c=1
function c(c){
    console.log(c)
    var c=3
}
c(2)
//c is not a function
//执行顺序 var c=undefined,c=function,c=1,c(2)

2.作用域链

https://github.com/mqyqingfeng/Blog/issues/8

1.概念

1.作用域规定了如何查找变量,也就是确定当前代码对变量的访问权限

函数的作用域在函数定义的时候就决定了

2.当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(执行上下文的变量对象)中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

举个例子:

 function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

执行过程如下:

1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]

checkscope.[[scope]] = [
    globalContext.VO
];

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

ECStack = [
    checkscopeContext,
    globalContext
];

3.checkscope 函数并不立刻执行,开始做准备工作

第一步:复制函数[[scope]]属性创建作用域链

checkscopeContext = {
    Scope: checkscope.[[scope]],
}

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

5.第三步:将活动对象压入 checkscope 作用域链顶端

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];

2.面试题

1.
//作用域链在一开始就确定了,找不到某个元素只会去它的上一级找
var x=10
function fn(){
    console.log(x)
}
function show(){
    var x=20
    fn()
}
show() //10
2.
//这里只有一个全局作用域和一个函数作用域,函数作用域没找到就去全局作用域找
var fn=function(){
    console.log(fn)
}
fn()  //f(){ console.log(fn) }
//这里只有一个全局作用域和一个函数作用域,函数作用域没找到就去全局作用域找
var obj={
    fn2:function(){
        console.log(fn2)
    }
}
obj.fn2() //fn2 is not define

3.什么时候沿着作用域链查找变量,什么时候通过原型链

1.通过变量名查找:会顺着作用域链查找到window,如果window也没有,就顺着原型链去查找window.prototype和Object.prototype

2.通过 . 或者[]查找: 顺着原型链查找

3.闭包

1.定义

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

自由变量是指在函数中使用的,但既不是函数参数也不是函数局部变量的变量。

2.必刷题

接下来,看这道刷题必刷,面试必考的闭包题:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

答案是都是 3,让我们分析一下原因:

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。data[1] 和 data[2] 是一样的道理。

所以让我们改成闭包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

跟没改之前一模一样。

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

匿名函数执行上下文的AO为:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。

4.参数按值传递

参数传递复制的是值,如果是变量复制的就是变量本身,如果是对象复制的是对象的引用

//obj=0x123
var obj = {
    value: 1
};
function foo(o) { //0=0x123
    o.value = 2;
    o.a=3
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
console.log(obj.a)    //3
//obj=0x123
var obj = {
    value: 1
};
function foo(o) { //0=0x123
    o = 2;			// 0=2
    console.log(o); //2
}
foo(obj);
//o复制的是obj的地址,o=0x123,obj=0x123,o=2不会印象obj
console.log(obj.value) // 1

this/call/apply/bind/new/深拷贝

1.this

this指向调用它的对象,如果没有则指向window

function Sub(){
    this.subprop='Sub prop'
}
Sub.prototype.showSub=function(){ 
    console.log(this.subprop)
}

var sub =new Sub()
sub.showSub()  //'Sub prop'    对象调用showSub,所以this是sub

特殊情况(可以不了解):

详细请看:https://github.com/mqyqingfeng/Blog/issues/7

//"use strict"   
//如果开启严格模式那么最下面的三段输出都会报错,非严格模式下this为undefined的时候会被转化为全局对象,所以我们可以认为this指向调用它的对象,如果没有则指向window
var value = 1;
var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

console.log(foo.bar());  //2
console.log((foo.bar)());//2

console.log((foo.bar = foo.bar)());//1
console.log((false || foo.bar)());//1
console.log((foo.bar, foo.bar)());//1

2.call和apply

1.call和apply定义:

**函数 **使用指定的this和指定的参数 来调用

如:bar.call(foo) 简单说就是 在foo里面调用bar函数

2.call和apply有什么区别:

call接收的是连续的参数,参数可以是任意的类型,包括数组

apply接收的是数组

3.如何实现call和apply

1.call
简易的理解call
// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn
第一版:
Function.prototype.call2 = function(context) {
    //console.log(this)  // f bar()
    // 首先要获取调用call的函数,用this获取(谁调用,this就指向谁)
    context.fn = this;
    context.fn();
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call2(foo); // 1
第二版:

解决call函数的参数,如bar.call(foo,‘kevin’,18)

Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push(`${arguments[i]}`);
    }
    //console.log(args)
    context.fn(...args)//...取出所有可以遍历的属性,拷贝到参数上
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call2(foo, 'kevin', 18); 
// kevin
// 18
// 1
最终版:

解决函数的返回值与传入参数为null的问题

1.原版bar.call(null) 会在window执行bar

2.原版bar.call(foo) bar如果不是函数,会报错

3.原版bar.call(foo) foo如果是数字或字符串,会转化为对象

4.原版如果bar函数有返回值,那么bar.call(foo)也会有返回值

// 第三版
Function.prototype.call2 = function (context) {
    // 判断调用对象
    if (context === null || context === undefined) {
		context = window;
    } else {
		context = Object(context) || context;//传入的是数字或者别的
    }
    context.fn = this;

    var args = [...arguments].slice(1)
    let result = context.fn(...args);

    delete context.fn
    return result;
}

测试:

var value = 2;
var obj = {
    value: 1
}
function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call2(null); // 2
console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }
2.apply

apply只是参数和call不同

Function.prototype.apply2 = function(context) {
    // 判断调用对象
    if (context === null || context === undefined) {
		context = window;
    } else {
		context = Object(context) || context;//传入的是数字或者别的
    }
    context.fn = this;

  	let result = null;
    
 	 // 调用方法
  	if (arguments[1]){
        result=context.fn(...arguments[1])
    } else {
        result = context.fn();
    }

    delete context.fn;
    return result;
};

3.bind

1.传入参数:bind的时候可以传参,对bind返回的函数也可以传参,

如下面例子:函数需要传 name 和 age 两个参数,可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age!

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);

}
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18

第一版:

可以使用上面的例子验证!

Function.prototype.myBind = function(context) {
  // 获取参数
  var args = [...arguments].slice(1),
  fn = this;
  return function Fn() {
      	//args是mybind里面的参数,bindArgs是往bind返回的函数里传入的参数
    	let bindArgs=[...arguments].slice(0)     
       	//使用apply在context上执行函数bar,返回函数的返回值
        return fn.apply(context,args.concat(bindArgs))
  };
};

第二版:

原本使用bind返回的函数可以当成构造函数!这种情况下bind传入的this会失效,但是传入的参数依然生效。

举个例子:

var value = 2;
var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,因为new的时候 this 指向了 obj,所以this.habit生效,this.value返回undefined

// 第二版
Function.prototype.bind2 = function (context) {
    var fn = this;
    var args = [...arguments].slice(1);

    var fBound = function () {
        let bindArgs=[...arguments].slice(0) 
        
// 1.当作为构造函数时,this 指向实例,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值,如上面例子中的habit
 // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
    return fn.apply(
       this instanceof fBound ? this:context ,
       args.concat (bindArgs));
    }
    
    // 修改返回函数的 prototype ,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

第二版的缺陷:

就是这句:fBound.prototype = this.prototype

1.我们不能直接将bar的prototype赋给fBound,浅拷贝会导致修改fBound.prototype的时候也更改bar.prototype
2.所以我们使用 fBound.prototype = Object.create(this.prototype)
3.这句话的意思是:fBound.prototype.–proto-- = bar.prototype,这样fBound就可以找到object的原型但不影响
4.tips:在fb=new fBound()的时候,我们不用考虑fBound.prototype = Object.create(fb.prototype)的影响,因为最终new操作符会让fb.–proto–=fBound.protoype。详细看new操作符的手写。

最终版:

Function.prototype.bind2 = function (context) {
    var fn = this
    var args = [...arguments].slice(1)
    
    var fBound = function () {
        var bindArgs = [...arguments].slice(0)
        return fn.apply(
       		this instanceof fBound ? this:context ,
       		args.concat (bindArgs))
    }
	
    fBound.prototype = Object.create(this.prototype)
    return fBound
}

4.new

New操作符的实现原理

参考:https://github.com/mqyqingfeng/Blog/issues/13

new操作符的执行过程:

1.首先创建了一个新的空对象
2.设置原型,将对象的原型设置为函数的 prototype 对象。
3.让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
4.判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

第一版代码:

new是关键字,我们无法创造出跟它一样的写法:

使用 new : var person = new Otaku(……);
使用 objectFactory:var person = objectFactory(Otaku, ……)

function objectFactory() {
    var obj = new Object(),
    Constructor = [].shift.call(arguments);
	//arguments不是数组,要想使用数组的shift方法就得用call,
	//这时候Constructor等于arguments第一个参数也就是传入的构造函数,
    //shift影响原数组,arguments已经少了第一个参数了
    obj.__proto__ = Constructor.prototype;
    Constructor.apply(obj, arguments); //改变构造函数的this到obj 
    //apply后面接收的是数组,call接收的是一连串参数,所以这里用apply
    return obj;
};

//不足:第一版并没有考虑构造函数有返回值的情况
//如果构造函数返回的是对象,实例 person 中只能访问返回的对象中的属性
//如果返回的是基本类型,如:return 'haha',相当于没有返回值
function Otaku (name, age) {
    this.strength = 60;
    this.age = age;
    return {
        name: name,
        habit: 'Games'
    }
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // undefined
console.log(person.age) // undefined

最终版代码:

//3.第二版代码(最终版)
function objectFactory() {
    var obj = new Object(),
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;

    var ret = Constructor.apply(obj, arguments);//如果构造函数有返回值,用ret接收
    return typeof ret === 'object' ? ret : obj;//ret如果是引用数据类型,返回ret,否则返回obj
};

5.深浅拷贝

参考:https://github.com/axuebin/articles/issues/20

定义:

深浅拷贝都是对于引用数据类型而言的,浅拷贝就只是复制对象的引用,深拷贝才是真正地对对象的拷贝

1.=是浅拷贝

const originArray = [1,2,3,4,5];
const originObj = {a:'a',c:[1,2,3]};

const cloneArray = originArray; // [1,2,3,4,5]
const cloneObj = originObj;// {a:'a',c:Array[3]}

cloneArray.push(6);
cloneObj.a = {aa:'aa'};

console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]

console.log(cloneObj); // {a:{aa:'aa'},c:Array[3]}
console.log(originObj); // {a:{aa:'aa'},c:Array[3]}

2.使用JSON.stringify/parse方法进行的是深拷贝

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';  cloneObj.c = [1,1,1];   cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

//使用这种方法进行深拷贝 undefined、function、symbol 会在转换过程中被忽略
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "axuebin"}

3.使用递归手写深拷贝

//核心就是对每一层的数据都实现一次 创建对象->对象赋值 的操作
//我们必须先创建新的{}或[],然后给他们用 = 给他们进行浅拷贝赋值
//递归使得每一层的拷贝都是深拷贝
function deepClone(source){
    // 判断复制的目标是数组还是对象
  const targetObj = source.constructor === Array ? [] : {}; 
    
  for(let keys in source){ //for in获取的是键名,且是字符串形式
    if(source.hasOwnProperty(keys)){
      // source[keys]表示不为null,后面表示:值是如果是对象,就递归一下
      if(source[keys] && typeof source[keys] === 'object'){ 
        targetObj[keys] = deepClone(source[keys]);
      }
        else{ 
        // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}

测试:

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false
console.log(cloneObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

4.js里的拷贝方法

//4.1 concat(num1,num2) 返回拼接后的数组,不影响原数组  
//concat进行的是首层深拷贝,第一层与原数组互不影响,后面的层会影响
const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false

cloneArray.push(3)
//这里之所以用Json进行深拷贝,是因为如果console.log(cloneArray),console.log记录的是引用,控制台输出的时候他输出的是代码全部执行完后的cloneArray
console.log(JSON.parse(JSON.stringify(cloneArray)))  //[1,[1,2,3],{a:1},3]
console.log(JSON.parse(JSON.stringify(originArray)))  //[1,[1,2,3],{a:1}]

cloneArray[1].push(4);
cloneArray[2].a = 2; 
console.log(originArray); // [1,[1,2,3,4],{a:2}]

//4.2 slice 用于读取下标start(包含)到end(不包含)的数组,不改变原数组
//slice与concat一样,进行首层深拷贝

//4.3 扩展运算符... 
//扩展运算符进行的是首层深拷贝
const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};

const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]

const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}

//4.4 Object.assign
//Object.assign() 是首层深拷贝

5.手写首层深拷贝(其实就是完全深拷贝不进行递归…)

function shallowClone(source) {
  const targetObj = source.constructor === Array ? [] : {}; 
    
  for (let keys in source) { // 遍历目标
    if (source.hasOwnProperty(keys)) {
      targetObj[keys] = source[keys];
    }
  }
  return targetObj;
}

6.总结:

1.JavaScript 中数组和对象自带的拷贝方法、扩展运算符、Object.assign是“首层深拷贝”;
2.和 = 进行的是浅拷贝
3.JSON.stringify 实现的是深拷贝,但是对目标对象有要求;
4.若想真正意义上的深拷贝,请手写递归。

对象的创建/继承

1.对象创建模式

1.object构造函数模式

在变量不确定时使用

var obj=new Object()
obj.name='zhangsan'

2.使用字面量模式

如果创建多个对象会有大量重复的代码

obj={
    name:'zhangsan'
}

3.工厂模式

工厂模式就是通过函数返回对象

缺点:无法区别使用createPerson创建出来的对象与createDog创建出来的对象有什么区别,都是object,跟createPerson函数没有关系.它只是简单的封装了复用代码,而没有建立起对象和类型间的关系

function createPerson(name,age){
    obj={//对象解构赋值
        name,
        age
    }
    return obj
}
var p1=createPerson('zhangsan',11)
var p2=createPerson('lisi',11)
console.log(p1,p2)

4.自定义构造函数模式

使用new创建

构造函数模式相对于工厂模式的优点是:所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但这种创建方式为不同的对象跟别创建了相同的方法,如:p1和p2都添加了fun1方法,浪费了内存空间

function Person(name,age){
    this.name=name
    this.age=age
    this.fun1=function(){
        console.log(3)
    }
}
var p1=new Person('zhangsan',11)
var p2=new Person('lisi',12)

5.单独使用原型

这种方式能解决4里的浪费内存的问题,但同时也接收不了参数了

6.构造函数配合原型使用

这是最常见的方法,但是因为使用了两种不同的模式,所以对于代码的封装性不够好

function Person(name,age){
    this.name=name
    this.age=age
}
Person.prototype.fun1=function(){
    console.log(3)
}
var p1=new Person('zhangsan',11)
var p2=new Person('lisi',12)
console.log(p1)
console.log(p2)

7.动态原型模式

将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

2.继承模式

1.原型链继承

属性存在原型上,修改引用类型的数据时容易混乱。而且子类型实例化的时候不能往父类型里面传参。

function Father(){
    this.a='Father a'
}
Father.prototype.showFather=function(){
    console.log(this.a)
}
function Sun(){
    this.b='Sun b'
}
//关键:Sun的prototype指向Father的实例,不写这句会默认指向object的实例
Sun.prototype=new Father()
//让protorype里面的构造器指向Sun,不然会沿用之前的最终会指向Father
Sun.prototype.constructor=Sun
Sun.prototype.showSun=function(){
    console.log(this.b)
}

var s=new Sun()
s.showFather()//Father a
s.showSun()//Sun b

2.借用构造函数继承

缺点:会重复创建函数,相同的函数没有得到复用

//实际就是用call方法简化了代码量,实际还是那么多代码
function Person(name,age){
    this.name=name
    this.age=age
}
function Student(name,age,sex){
    //Person构造构造函数也是函数!用这里的this调用Person函数
    Person.call(this,name,age)
    this.sex=sex 
}

3.组合继承(常用)

使用原型继承和构造函数继承相结合。

理解:变量是不同的,需要单独指定,但可以用call减少代码量。方法可以是相同的,使用原型链

缺点:由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

function Father(age,name){
    this.age=age
    this.name=name
}
Father.prototype.showFather=function(){ }
function Sun(age,name,sex){
    Father.call(this,age,name)
    this.sex=sex
}

Sun.prototype=new Father()//用Father生成实例对象作为Sun的原型对象
Sun.prototype.constructor=Sun//让protorype里面的构造器指向Sun
Sun.prototype.showSun=function(){}

4.原型式继承

原型式继承的主要思路是可以基于已有的对象创建新的对象。

Object.create() 方法就是下面代码的规范化。

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

与原型链方式相同,缺点是修改引用类型的数据时容易混乱。而且子类型实例化的时候不能往父类型里面传参。

5. 寄生式继承

寄生式继承的思路是,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回这个对象.

缺点是没有办法实现函数的复用。

function createAnother(original) {
  let clone = Object.create(original);
  clone.sayHi = function () {
    console.log("hi");
  };
  return clone;
}

6.寄生式组合继承

综合4、5对3进行改造,得到的6是最理想的继承方式

寄生式组合继承与组合继承不同的地方主要是:在继承原型时,我们继承的不是超类的实例对象,而是原型对象是超类原型对象一个实例对象这样就解决了基类的原型对象中增添了不必要的超类的实例对象中的所有属性的问题

简单说就是:因为new的时候会运行父亲里的代码,我们不让Sun.prototype=new Father(),我们让Sun.prototype=Object.create(Father.prototype)

function inheritPrototype(subType, superType) {
  // 创建对象  创建原型对象是超类原型对象的  一个实例对象
  let prototype = Object.create(superType.prototype);
  // 修改prototype的constructor为subType
  prototype.constructor = subType;
  // 赋值
  subType.prototype = prototype;
}
function Father(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

Father.prototype.sayName = function () {
  console.log(this.name);
};

function Son(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

inheritPrototype(Son, Father);

SubType.prototype.sayAge = function () {
  console.log(this.age);
};

事件循环模型

详解:https://juejin.cn/post/6844903512845860872

循环的理解:这里介绍的是浏览器的执行机制,在node或ringo等执行机制会不同。运行机制大多指js解析引擎,是统一的。

主程序执行完后,就一直在等待任务队列的任务,settimeout等会直接拿出来执行,onclick等会一直在队列中,等你触发相应的事件后就会执行

4.Javascript高级_第3张图片

settime理解:遇到settime声明,就将函数放到event table,过了指定的时间就将函数放入event queue

,所以等待时间大于主程序运行时间它几乎是准确的,而如果它放入了event queue中后主程序还没运行完,那么要等待主程序运行完才能运行event queue里的内容。

setinterval理解:每隔一段时间将要执行的函数加入到event queue

settimeout 和setintelout属于进入宏任务的event queue

new promise 立即执行,多层new也是立即执行

promise.then和process.nextTick进入微任务的event queue

4.Javascript高级_第4张图片

每执行完一个宏任务会去执行所有的微任务,最开始window上的代码执行完也算执行完了一段宏任务,执行完setTimeout里面的回调也算是执行完了一段宏任务.

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
//1,7,6,8,2,4,3,5,9,11,10,12

异步编程

1.异步编程的实现方式

1.回调函数

使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱

2.Promise

使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。

3.generator

4.async函数

async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

你可能感兴趣的:(js及面试题,javascript,原型模式,前端)