JS基础

195 阅读2分钟

语言基础

变量

var 声明提升

  • 变量提升 var a = 2
    • JavaScript 实际上会将其看成两个声明:
      • var a;
      • a = 2;
      • 第一个定义声明是在编译阶段进行的。
      • 第二个赋值声明会被留在原地等待执行阶段。
  • 函数提升
    foo();
    function foo() { 
      console.log( a ); // undefined 
      var a = 2; 
    }
    // 相当于下面的代码(作用域都会进行提升操作)
    function foo() { 
      var a; 
      console.log( a ); // undefined
      a = 2;
    }
    foo();
    
  • 函数优先
    foo(); // 1
    var foo;
    function foo() {
      console.log( 1 );
    } 
    foo = function() {
      console.log( 2 );
    };
    
    // 相当于
    function foo() {
      console.log( 1 );
    } 
    foo(); // 1
    foo = function() {
      console.log( 2 );
    };
    
    注意,var foo 尽管出现在 function foo()... 的声明之前,但它是重复的声明(因此被忽略了), 因为函数声明会被提升到普通变量之前。

数据类型

  • 基本数据类型:Undefined、Null、Boolean、Number、String、Symbol、BigInt
  • 复杂数据类型:Object

typeof用于确定数据类型

对一个值使用 typeof 操作符会返回下列字符串之一:

  • "undefined"表示值未定义;
  • "boolean"表示值为布尔值;
  • "string"表示值为字符串;
  • "number"表示值为数值;
  • "object"表示值为对象(而不是函数)或 null;
  • "function"表示值为函数;
  • "symbol"表示值为符号。
console.log(
    typeof 100, //"number"
    typeof 'abc', //"string"
    typeof false, //"boolean"
    typeof undefined, //"undefined"
    typeof null, //"object"
    typeof [1,2,3], //"object"
    typeof {a:1,b:2,c:3}, //"object"
    typeof function(){console.log('aaa');}, //"function"
    typeof new Date(), //"object"
    typeof /^[a-zA-Z]{5,20}$/, //"object"
    typeof new Error(), //"object"
    typeof new Number(100), //'object'
    typeof new String('abc'),// 'object'
    typeof new Boolean(true),//'object'
);

引用数据类型中的:Array,Object,Date,RegExp。不可以用typeof检测。都会返回小写的object
具体来说,new String(‘abc’) 创建了一个字符串对象,而不是一个基本类型的字符串。这意味着这个对象具有 String 类型的所有属性和方法,可以通过点操作符访问。 字符串对象是一个包装对象,它将基本类型的字符串包装起来,使其具备对象的特性。对于普通的字符串字面量,例如 ‘abc’,它们是基本类型的字符串,而不是对象,其 typeof 结果是 “string”。 所以,typeof new String(‘abc’) 返回的是 “object”,表示这是一个对象类型的值。如果要判断一个对象是否是字符串对象,可以使用 instanceof 运算符,如:new String(‘abc’) instanceof String,它将返回 true。

instanceof

typeof 虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象, 而是想知道它是什么类型的对象。为了解决这个问题,ECMAScript 提供了 instanceof 操作符
如果变量是给定引用类型(由其原型链决定,将在第 8 章详细介绍)的实例,则 instanceof 操作符返回 true。来看下面的例子:

console.log(person instanceof Object); // 变量 person 是 Object 吗
console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?

按照定义,所有引用值都是 Object 的实例,因此通过 instanceof 操作符检测任何引用值和 Object 构造函数都会返回 true。类似地,如果用 instanceof 检测原始值,则始终会返回 false, 因为原始值不是对象。

console.log(
    100 instanceof Number, //false
    'dsfsf' instanceof String, //false
    false instanceof Boolean, //false
    undefined instanceof Object, //false
    null instanceof Object, //false
    [1,2,3] instanceof Array, //true
    {a:1,b:2,c:3} instanceof Object, //true
    function(){console.log('aaa');} instanceof Function, //true
    new Date() instanceof Date, //true
    /^[a-zA-Z]{5,20}$/ instanceof RegExp, //true
    new Error() instanceof Error //true
)

Object.prototype.toString.call()

var toString = Object.prototype.toString;
toString.call(123); //"[object Number]"
toString.call('abcdef'); //"[object String]"
toString.call(true); //"[object Boolean]"
toString.call([1, 2, 3, 4]); //"[object Array]"
toString.call({name:'wenzi', age:25}); //"[object Object]"
toString.call(function(){ console.log('this is function'); }); //"[object Function]"
toString.call(undefined); //"[object Undefined]"
toString.call(null); //"[object Null]"
toString.call(new Date()); //"[object Date]"
toString.call(/^[a-zA-Z]{5,20}$/); //"[object RegExp]"
toString.call(new Error()); //"[object Error]"

Undefined

  1. 无论是声明还是未声明,typeof 返回的都是字符串"undefined"。
    let message; // 这个变量被声明了,只是值为 undefined 
    // 确保没有声明过这个变量 
    // let age
    console.log(typeof message); // "undefined" 
    console.log(typeof age); // "undefined"
    

Null

  1. 逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因:
    let car = null; 
    console.log(typeof car); // "object"
    
  2. 底层原理:null作为一个基本数据类型为什么会被typeof运算符识别为object类型呢? 这是因为javascript中不同对象在底层都表示为二进制,而javascript 中会把二进制前三位都为0的判断为object类型,而null的二进制表示全都是0,自然前三位也是0,所以执行typeof时会返回'object。 ----引用自《你不知道的javascript(上卷)》
  3. undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等
     console.log(null == undefined); // true
    

Boolean

  1. 不同类型与布尔值之间的转换规则 image.png

Number

  1. 八进制:第一个数字必须是零(0),然后是相应的八进制数字(数值 0~7)比如:070 代表十进制 56 (严格模式下是无效的应该使用前缀 0o)
  2. 十六进制:必须让真正的数值前缀 0x(区分大小写),然后是十六进制数字(0到9 以及 A到F)比如:0x1f 代表十进制 31
  3. 科学记数法:格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10 的多少次幂比如:23e5 代表 23 * 10 的5次方 = 2300000
  4. 小数精度问题:0.1 + 0.2 不等于 0.3,原因:计算机将数字转成二进制计算,再转换回来丢失精度,解决方案:将小数乘以 一定倍数转成整数后计算,再除以对应倍数还原
  5. 特殊的数值NaN:用于表示本来要返回数值的操作失败了(而不是抛出错误)
    console.log(0/0); // NaN
    console.log(-0/+0); // NaN
    
    // 如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity 或-Infinity:
    console.log(5/0); // Infinity console.log(5/-0); // -Infinity
    
    // NaN 不等于包括 NaN 在内的任何值
    console.log(NaN == NaN); // false、
    
    // isNaN()函数接收一个参数,该函数会尝试把它转换为数值,判断这个参数是否NaN
    console.log(isNaN(NaN)); // true
    console.log(isNaN(10));// false,10 是数值
    console.log(isNaN("10")); // false,可以转换为数值 10
    console.log(isNaN("blue")); // true,不可以转换为数值
    console.log(isNaN(true)); // false,可以转换为数值 1
    
  6. 数值转换
  • Number()
    • 布尔值,true 转换为 1,false 转换为 0。
    • 数值,直接返回。
    • null,返回 0。
    • undefined,返回 NaN
    • 字符串
    • 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换。
  • parseInt()
    • 从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回 NaN。
    let num1 = parseInt("1234blue");     // 1234
    let num2 = parseInt("");             // NaN
    let num3 = parseInt("0xA");          // 10,解释为十六进制整数
    let num4 = parseInt(22.5);           // 22
    let num5 = parseInt("70");           // 70,解释为十进制值
    let num6 = parseInt("0xf");          // 15,解释为十六进制整数
    
  • parseInt()也接收第二个参数,用于指定底数(进制数)
    let num1 = parseInt("10", 2);        // 2,按二进制解析
    let num2 = parseInt("10", 8);        // 8,按八进制解析
    let num3 = parseInt("10", 10);       // 10,按十进制解析
    let num4 = parseInt("10", 16);       // 16,按十六进制解析
    
    • parseFloat() 跟 parseInt()函数类似
    let num1 = parseFloat("1234blue");   // 1234,按整数解析
    let num2 = parseFloat("0xA");        // 0 
    let num3 = parseFloat("22.5");       // 22.5 
    let num4 = parseFloat("22.34.5");    // 22.34 
    let num5 = parseFloat("0908.5");     // 908.5 
    let num6 = parseFloat("3.125e7");    // 31250000
    
    • 需要注意的是
    console.log(parseInt('')); // NaN
    console.log(Number('')); // 0
    

String

  • toString()方法可见于数值、布尔值、对象和字符串值。(null 和 undefined 值没有)
  • toString()可以接收一个底数参数
let num = 10;
console.log(num.toString());                   // "10" 
console.log(num.toString(2));                  // "1010" 
console.log(num.toString(8));                  // "12" 
console.log(num.toString(10));                 // "10" 
console.log(num.toString(16));                 // "a"
  • String()
    • 如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
    • 如果值是 null或者undefined,返回"null"或者"undefined"。

Symbol

  • 符号是原始值,且符号实例是唯一、不可变的。
  • 用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。符号就是用来创建唯一记号,进而用作非 字符串形式的对象属性。
var a1 = Symbol('age');
 var c = {
 [a1]: 18
}
c.age = 20;

类似于 Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键

Object

  • 对象其实就是一组数据和功能的集合 image.png
console.log({}.toString());     // [object Object]
console.log([].toString());     // ''
console.log({}.valueOf());      // {}
console.log([].valueOf());      // []

//下⾯代码分别单独执⾏各输出什么?
{}.toString() // 报错
[].toString() // ''
6.toString() // 报错

// js 引擎在执行时,遇到 {,至少有两种选择,当做语句块的开始当做对象字面量表达式的开始但是,默认情况下,是当做语句块的,所以{}.toString()会报错

// S对`.`号有两种理解, 一种是小数点, 另一种是访问属性。而在JS中数字如果直接跟点就会被认为是小数点,而不是访问属性

操作符

相等操作符

  1. 等于和不等于
  • 转换规则
      1. 数值与布尔值,将布尔值转换为数值
      1. 数值与字符串,将字符串转换为数值
      1. 一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较。(这步是JS解释器执行的,ToPrimitive方法的实现,正是依次去调用对象的valueOf,toString方法,直到其中一个方法返回一个基本值,然后比较返回的基本值和另一侧的值。如果这两个方法没有返回基本值 ,那就认定不相等 )
      1. null 和 undefined 相等。
      1. null 和 undefined 不能转换为其他类型的值再进行比较
      1. 如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。
      1. 如果两个操作数都是对象,则比较它们是不是同一个对象。
      1. 如果任一值是 true,把它转换成 1 再比较;如果任一值是 false,把它转换成 0再比较
    console.log(null == undefined);                // true
    console.log(NaN == NaN);                       // false
    console.log(NaN != NaN);                       // true
    console.log(undefined == 0);                   // false
    console.log(null == 0);                        // false
    console.log(null == undefined);                // true
    console.log([1] == [1]);                       // false
    console.log([] == false);                      // true
    console.log([1] == '1');                       // true
    console.log([] == '');                         // true
    console.log({} == '');                         // false
    console.log({} == '[object Object]');          // true
    console.log('' == false);                      // true
    
  • 一道有意思的题目
    // a=? a等于什么下方等式成立
    console.log(a==1&&a==2&&a==3)  //true
    
    
    //  答案
    const a = (function() {
    let i = 1;
    return {
        valueOf: function() {
            return i++;
        }
    }
    })();
    
    // 或者
    var a = {
      i: 1,
      toString() {
        return a.i++
      }
    }
    
    console.log(a==1&&a==2&&a==3)  //true
    
  1. 全等和不全等
    全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操作符由3 个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回 true。

作用域

  • 作用域:作用域是一套规则,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。用于确定在何处以及如何查找变量(标识符)
  • 作用域嵌套:当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”
  • 词法作用域:简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。但是存在欺骗词法(不推荐,性能也不好)
    • 欺骗词法: eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。还有with这里就不说了
    function foo(str, a) {
        eval(str); // 欺骗!
        console.log(a, b);
    }
      var b = 2;
      foo("var b = 3;", 1); // 1, 3
    
  • 动态作用域: 而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调 用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。

执行上下文与作用域

变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。

  • 全局上下文是最外层的上下文。在浏览器中,全局上下文就是我们常说的 window 对象,全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器
  • 每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。
  • 上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有 一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。

闭包

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • 当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
  • 上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。
  • 闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

闭包的作用

  • 节流与防抖
  • 函数柯里化

对象

创建对象

  1. 字面量对象
const obj = { a: 1 }
  1. 通过 new Object 声明一个对象
const obj = new Object({ a: 1 })

虽然使用 Object 构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。

工厂模式

function createPerson(name, age) {
    const obj = new Object()
    obj.name = name
    obj.age = age
    obj.sayName = function() {
        console.log(obj.name);
    }
    return obj
}
const p1 = createPerson('xiaoming',18)
console.log(p1); // { name: 'xiaoming', age: 18, sayName: [Function (anonymous)] }

这种工厂模式虽 然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

构造函数模式


function Person(name, age) {
    this.name = name
    this.age = age
    this.sayName = function() {
        console.log(this.name);
    }
}
const p1 = new Person('xiaoming',18)
const p2 = new Person('xiaohong',18)
console.log(p1)  // Person { name: 'xiaoming', age: 18, sayName: [Function (anonymous)] }

构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对前面的例子而言,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例,解决办法如下

function Person(name, age) {
    this.name = name
    this.age = age
    this.sayName = sayName
}
function sayName() {
    console.log(this.name)
}
const p1 = new Person('xiaoming',18)
const p2 = new Person('xiaohong',18)
p1.sayName() // xiaoming

这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。

原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享

function Person() { }
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.sayName = function () {
    console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true 
  • prototype
    无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。
  • __proto__
    实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。脚本中没有访问这个[[Prototype]]特性的标准方式,但 Firefox、Safari 和 Chrome 会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。
    console.log(Person.prototype.constructor === Person); // true
    
    /** 
    * 正常的原型链都会终止于 Object 的原型对象 
    * Object 原型的原型是 null 
    */ 
    console.log(Person.prototype.__proto__ === Object.prototype); // true
    console.log(Person.prototype.__proto__.constructor === Object); // true
    console.log(Person.prototype.__proto__.__proto__ === null); // true
    
    /** 
    * 实例通过__proto__链接到原型对象, 
    * 它实际上指向隐藏特性[[Prototype]] 
    * 构造函数通过 prototype 属性链接到原型对象 
    * 实例与构造函数没有直接联系,与原型对象有直接联系 
    */
    console.log(person1.__proto__ === Person.prototype); // true
    conosle.log(person1.__proto__.constructor === Person); // true
    

image.png

  • Object.create()创建一个新对象,同时为其指定原型
    let biped = { numLegs: 2 };
    let person = Object.create(biped);
    person.name = 'Matt';
    console.log(person.name); // Matt
    console.log(person.numLegs); // 2
    console.log(Object.getPrototypeOf(person) === biped); // true
    
  • 原型层级
    如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性。只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。
    hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。返回true说明在实例上
    console.log(person1.hasOwnProperty("name"))
    
  • 原型和 in 操作符
    in 操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上
    // 确定某个属性是否存在于原型上
    function hasPrototypeProperty(object, name){
      return !object.hasOwnProperty(name) && (name in object); 
    }
    
  • 原型的问题
    由于原型上的属性时所有实例共享的,当原型的某个属性的属性值是引用值的时候,所有实例会共享这个引用值,但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。

继承

  • 原型链继承
    思路:子类构造函数的原型指向父类构造函数的实例
function SuperType() {}
function SubType() {}
subType.prototype = new superType()

问题:也就是前面提到的原型的问题,原型中包含的引用值会在所有的实例之间共享。第二个问题就是子类型在实例化时不能给父类型的构造函数传参。

  • 盗用构造函数
    思路:在子类构造函数中调用父类构造函数,在
function SuperType () {
  this.color = ['red', 'blue' ]
}
function SubType () {
  // 继承SuperType 
  SuperType.call(this)
}

通过使用 call()(或 apply())方法,SuperType 构造函数在为 SubType 的实例创建的新对象的上下文中执行了。这相当于新的 SubType 对象上运行了 SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的 colors 属性。
参数传递: 比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参

function SuperTypr (name) {
  this.name = name
}
function SubType() {
  // 继承SuperType并传参
  SuperTypr.call(this, 'SubTypeName')
  // 实例属性
  this.age = 18
}

问题:也就是上面提到的构造函数模式创建类型的问题,必须在构造函数中定义方法,因此函数不能重用。第二就是子类不能访问父类型原型上定义的方法。

  • 组合继承
    思路:综合原型链和盗用构造函数,使用原型链继承原型上的属性和方法,再通过盗用构造函数基础实例属性。
function SuperTypr (name) {
  this.name = name
}
SuperTypr.prototype.sayName = function() {
  console.log(this.name);
}
function SubType() {
  // 继承属性
  SuperTypr.call(this, 'SubTypeName')
}
// 继承方法
SubType.prototype = new SuperType()

问题:父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。有两组属性:一组在实例上,另一组在 SubType 的原型上。这是调用两次 SuperType 构造函数的结果。

  • 原型式继承
    使用了Object.create(),这个方法接收两个 参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。第二个参数与 Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。
let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
 };
 let anotherPerson = Object.create(person, {
  name: {
    value: "Greg"
  },
  age: {
    value: 18
  }
 });
 console.log(anotherPerson.name); // "Greg"
 console.log(anotherPerson.name); // 18

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住, 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

  • 寄生式继承
    思路:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
let parent = {
   name: "Nicholas", 
   friends: ["Shelby", "Court", "Van"]
}
function clone(src){
  let obj = Object.create(src)
  obj.getName = function(){
    return this.name
  }
}
let p1 =clone(parent)
  • 寄生式组合继承
function inheritPrototype(subType, superType) {
  let 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)
}

new 操作符实例化

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
function _new(Ctor, ...params) {
    // 1 2
    let obj = Object.create(Ctor.prototype)
    // 3 4
    let res = Ctor.call(obj, ...params)
    // 5
    if (res !== null && /^(object|function)$/.test(typeof res)) return res
    return obj
}

补充:new 与有括号配合时,先执行 new 操作符,后执行 . 点符号

var getName = function () { console.log('张三') }
function A () {
  return this
}
var x = A.getName = function () { console.log('李四') }
A.prototype.getName = function () { console.log('王五') }
new A.getName()  // 李四
new A().getName() // 王五