JavaScript 大杂烩

126 阅读14分钟

一:JS相关、JS变量、JS值

1. 单线程怎么模拟多线程?

轮转时间片:短时间之内轮流执行多个任务的片段。

  • 一共有两个任务:任务1、任务2
  • 切分任务1、任务2。
  • 随机排列这些任务片段,组成队列
  • 按照这个队列顺序将任务片段送到JS进程
  • JS线程执行一个又一个的任务片段

2. 关于script

写法

  • 代码可以写在 script 标签内部, 也可以通过 src 引入外部链接。但是外部导入跟内部写 script 只能生效外部导入的 js
<script type="text/javascript" src="index.js">
  console.log('不生效')
</script>
  • 故意写错 type 做模版 通过 id 获取 innerHTML 做模版替换
<script type="text/tpl" id="tpl">
   <div class="name">{{name}}</div>
</script>

堵塞机制

WechatIMG940.jpeg

  • script:直接使用script标签的话,html会按照它出现的顺序依次加载&运行它。在加载&运行的过程中,会阻塞后续的DOM渲染。所以建议把script标签放在<body>元素中页面内容的后面,也就是</body>前。 为了解决script会引起页面堵塞的问题,出现了两个属性 deferasync ,这两个都不会堵塞页面DOM的渲染。

  • defer:只适用于外部脚本。如果script标签使用了defer属性,就代表告诉浏览器异步的立即下载但延迟执行。不会影响后续DOM的渲染。脚本会被延迟到整个页面都解析完毕后在运行。会在DOMContentLoaded事件之前执行。能保证执行顺序(如果设置了多个,会按照顺序执行)。

    ⚠️ 在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在DOMContentLoaded事件触发前执 行,因此最好只包含一个延迟脚本。

  • async: 只适用于外部脚本。如果script标签使用了async属性,浏览器会立即下载相应的脚本,在下载的过程中页面的处理不会停止,下载完成后立即执行,执行过程中页面处理会停止。会在页面的load事件前执行,有可能会在DOMContentLoaded 之前或之后。不能保证执行顺序。适合没有依赖关系的脚本。

deferasync的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

如果同时指定了两个属性,则会遵从async属性而忽略defer属性。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>script标签</title>
    <script type="application/javascript" defer src="test.js"></script>
</head>
<body>
   <div class="container">
        // 10000个img标签 这里就省略不写了
        <img src="down.png" alt="">
        ...
   </div>
   <script>
        window.addEventListener('DOMContentLoaded', function () {
          console.log('DOMContentLoaded');
        });
        window.addEventListener('load', function () {
          console.log('load');
        })
    </script>     
</body>
var imgs = document.querySelectorAll('img'); 
console.log(imgs);

如果使用defer标签,输出的结果是:

1.jpeg

如果使用async属性,输出的结果是:

2.jpeg

上面的测试可以证明:async下载后立即执行,在执行的过程中是会堵塞页面渲染的;

3. 栈内存、堆内存

  • 栈内存:原始值存在栈内存中,重新赋值的时候复制原来的值又开辟一个新空间。不会相互影响;
var a = 3;
var b = a; 
a = 1; 
console.log(b); //3
  • 堆内存:引用值的内容存在堆内存,地址存在栈内存,赋值的时候指向同一个堆内存的地址;
var arr = [1,2,3,4]; 
var arr1 = arr; 
arr.push(5);
console.log(arr1); // [1,2,3,4,5]

重新赋值的话,重新开辟一个新的堆内存存储新的值。栈内存存储新的地址;

var arr = [1,2,3,4];
var arr1 = arr;
arr.push(5);
arr = [1,2];
console.log(arr1); //[1,2,3,4,5]

二:基本语法规范、运算符、判断

1. 错误相关

  • 语法错误(syntaxError):会导致所有语句都不会执行;
console.log(1);
console.log(2);// 这里用的中文的分号,通篇的语句都不会执行;
  • 引用错误(ReferenceError):错误之前的语句可以执行,之后的语句不会执行;
console.log(1); //可以执行
console.log(a); //报错 ReferenceError
console.log(2); //不会执行
  • 类型错误(TypeError):进行了不合理的非法操作; 比如对一个非函数类型的值进行函数调用;或者引用null或undefined类型的值中的属性;
null.toString(); // TypeError

不同的代码脚本块错误不会互相影响;

<script type="text/javascript">
  console.log(a); // 错误 ReferenceError
</script>
<script type="text/javascript">
  console.log(1); // 可以执行
</script>

2. 运算

console.log(0/0);  // NaN
console.log(1/0);  // Infinity
console.log(-1/0); // -Infinity

(1):如何通过运算交换两个值(前提是不引入多余的变量)?

var a = 1, b = 2;
a = a + b; //3
b = a - b; //1
a = a - b; //2

(2):自增、自减运算

  • ++a; --a:先运算后打印;
var a = 1;
console.log(++a); //2
  • a++; a--;先打印后赋值;
var a = 1; 
console.log(a++); //1
console.log(a); //2
var a = 5, b;
b = a++ + 1; // b = 1 + a++;
console.log(a, b); //6 6

b = a-- + --a; // b = --a + a--;
console.log(a, b); //3 8

(3):比较运算符

  • numberstring比较: string 会先转成数字进行比较;
  • stringstring比较: 会比较 ASCII 码;多个字符的,从左到右依次对比,直到比较出 ASCII 大小为止(如果第一位就不同直接出结果,如果相同比较第二位)
console.log('4.1' > '11');  // true
console.log('1.1' > '11');  // false

(4):位运算

  • 按位非(~):返回数值的一补数(反转每一位的二进制值)。最终结果是对数值取反并减1;
let num1 = 25;
let num2 = ~num1;
console.log(num2);// -26
  • 按位与(&):两个位都是1时返回1,在任何一位是0时返回0;
let result = 25 & 3;
console.log(result); //1
  • 按位或(|):至少一位是1时返回1,两位都是0时返回0;
let result = 25 & 3;
console.log(result); //27
  • 按位异或(^):只在一位上是1的时候返回1(两位都是1或0,返回0); 自身异或自身的话返回0;自身异或0的话返回自身。

找出数组中仅出现一次的元素就可以使用异或;

function test(arr){
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    sum = sum ^ arr[i];
  }
  return sum;
}
console.log(test([1,6,3,2,8,2,1,6,3]));
  • 左移(<<): 按照指定的位数将数值的所有位向左移动,会保留它所操作数值的符号;
let oldValue = 2;
console.log(oldValue << 5); //64
  • 有符号右移(>>): 左移的逆运算;
let oldValue = 64;
console.log(oldValue >> 5); //2
  • 无符号右移(>>>):对于正数来说,跟有符号右移结果相同。对于负数,当成正数的二进制来处理;
let oldValue = -64;
console.log(oldValue >>> 5); //134217726

3. 判断

  • switch语句:括号中填1,2等,走 defalut ,如果没有 default 打印 undefined ;填 true 是正确的结果;填 false 只会走第一个 case;
// 1
var score = 82;
switch (1) {
  case score >= 90 && score <= 100:
    console.log('优');
    break;
  case score >= 80 && score < 90:
    console.log('良');
    break;
  case score >= 70 && score < 70:
    console.log('一般');
    break;
  default:
    console.log('差')
}
// 打印出来的是差
// true
var score = 82;
switch (true) {
  case score >= 90 && score <= 100:
    console.log('优');
    break;
  case score >= 80 && score < 90:
    console.log('良');
    break;
  case score >= 70 && score < 70:
    console.log('一般');
    break;
  default:
    console.log('差')
}
// 打印出来的是良
// false
var score = 82;
switch (false) {
  case score >= 90 && score <= 100:
    console.log('优');
    break;
  case score >= 80 && score < 90:
    console.log('良');
    break;
  case score >= 70 && score < 70:
    console.log('一般');
    break;
  default:
    console.log('差')
}
// 打印出来的是优
  • &&、|| &&:遇到真就往后走,遇到假或走到最后就返回当前的值;
console.log(1 && 2 && undefined && 10); // undefined

||:遇到假就往后走,遇到真或走到最后就返回当前的值;

console.log(0 || null || 1 || 0); //1

三:循环、引用值、类型转换

1. 循环

  • 打印0-100的数:()只能有一句,不能写比较;{}不能出现i++ i--;
var i = 101;
for (; i--;) { //for循环的第二个参数是判断条件 i=0的时候会退出
  console.log(i);
}
  • 打印100以内的质数(仅能被1和自己整除的数);
var c = 0;
for (var i = 2; i < 100; i++) {
  for (var j = 1; j <= i; j++) {
    if (i % j == 0) {
      c++;
    }
  }
  
  // 只能被1跟自己整除 所以次数是2
  if (c == 2) {
    console.log(i)
  }
  c = 0;
}

for...in、for...of

  • for..in..:用于枚举对象中的非符号键属性,包括继承的可枚举属性;不能保证对象属性的返回顺序。不能用来遍历 mapset 在谷歌下,先按数值升序,在按插入字符串的顺序遍历的。符号类型的属性会被忽略。 for..in..遍历跟Object.keys()一致。
let obj = {
    1: 1,
    first: 'first',
    [Symbol('k1')]: 'symbolk1',
    third: 'third',
    0: 0,
    [Symbol('k2')]: 'symbolk2',
    2: 2,
    second: 'second',
  };
  for(let key in obj){
    console.log(key); // 0,1,2,first,third,second
  }
 console.log(Object.keys(obj)); ['0','1','2','first','third','second']
  • for..of..:用于可迭代对象(包括 ArrayMapSetStringTypedArrayarguments) 对象等等)。按照顺序迭代元素;
var obj = {
  a: 1,
  b: 2
};
for (var key in obj){
  console.log(key); // 打印键名 a,b
}
for (var key of obj){
  console.log(key); // Uncaught TypeError: obj is not iterable
}

var arr = [1,2,3,4,5];
for (var key in arr){
  console.log(key); // 打印出数组下标
}
for (var key of arr){
  console.log(key); // 打印正确的值
}


let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (let entry of iterable) {
  console.log(entry);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]
for (let [key, value] of iterable) {
  console.log(value);
}
// 1
// 2
// 3

2. 引用值

  • typeof() 返回的结果总是 string 类型;
typeof(typeof(123)); //string

3. 类型转换

Number('a'); //NaN
Number(undefined); //NaN
Number(null); // 0
  • parseInt:从第一个非空格字符开始转换。(字符串最前面的空格会被忽略)。如果第一个字符不是数字符号、加号、减号,会立即返回 NaN(空字符串也会返回)。如果第一个字符是数字符号、加号、减号,则依次监测每个字符,直到末尾或碰到非数值字符。不会四舍五入; 可以指定底数 radix
parseInt(true); //NaN
parseInt('3.99'); //3 会忽略小数后面的,不会四舍五入
parseInt('adc123'); // NaN 第一位不是数字的话直接NaN
parseInt('123abc'); // 123 遇到不是数字的就跳出了
parseInt('+12'); //12
parseInt(' '); //NaN
parseInt('+132-13'); //132
parseInt('00ff', 16); //255
  • parseFloat: **会四舍五入;只能解析十进制,不能指定底数。**其他的跟 parseInt 一样
parseFloat(true); //NaN
parseFloat('3.99'); //3 会忽略小数后面的,不会四舍五入
parseFloat('adc123'); // NaN 第一位不是数字的话直接NaN
parseFloat('123abc'); // 123 遇到不是数字的就跳出了
parseFloat('3.146242').toFixed(2); //3.15 会四舍五入
parseFloat('00ff', 16); //0
parseFloat('ff00', 16); //NaN
  • undefinednull undefinednull 没有 toString 方法,会报错;其他的原始值都有 toString 方法。但不是使用的 Object.prototype 原型的toString方法;都进行了重写;
var a = 1;
Object.prototype.toString.call(a); // '[object Number]'
Number.prototype.toString.call(a); // '1'

undefinednull 既不大于0也不小于0也不等于0;

console.log(null == 0); //false
console.log(undefined == 0); //false
console.log(undefined == null); //true
  • NaN

NaN 不等于包括 NaN 在内的任何值;

console.log(NaN == NaN); //false

isNaN:也会进行隐式类型转换,先 Number() 在对比;

isNaN('a'); //true
isNaN(null); //false
isNaN(undefined); //true

四:函数

1. 函数名

使用函数表达式定义函数的时候会自动忽略后面function的名字,外部不可见,但是可以在内部调用(递归);

foo(); //TypeError
bar(); //ReferenceError
var foo = function bar(){
    ...
}
// 这个代码片段经过提升之后,实际会被理解为以下形式:
var foo;
foo(); // 对undefined一个非函数的值调用函数会报TypeError
bar(); // 在当前作用域没有找到bar,报ReferenceError
foo = function(){
    var bar = ...
}
var test = function test1() {
  var a = 1, b = 2;
  console.log(a, b);
  // test1(); 不报错
};
console.log(test.name); //test1
test1(); //报错

2. 参数

形参跟实参数量可以不相等,哪个多了都不报错;

function test(a, b) {
  console.log(a, b); //1,2
}
test(1,2,3);

function test(a, b) {
  console.log(a, b); //1 undefined
}
test(1);

arguments 相关:

  • 实参的长度: arguments.length , 形参的长度:函数名.length
  • 实参里面传了值的话,函数内部可以更改实参的值;如果没传修改是没用的;
function test(a, b) {
  a = 3;
  console.log(arguments[0]); //3
}
test(1,2);

function test(a, b) {
  b = 3;
  console.log(arguments[1]); //undefined
}
test(1);
  • 实参跟形参一一对应,是两个东西,但是有映射关系
function b(x, y, a) {
  a = 10;
  console.log(arguments[2]); //10
}
b(1,2,3);

function b(x, y ,a) {
  arguments[2] = 10;
  console.log(a); //10
}
b(1,2,3);

五:预编译

1. GO、AO

  • GO:全局执行上下文 (1)找变量(var)

(2) 找函数声明

(3) 找到顶级let const class声明

(4)执行

  • AO:函数执行上下文 (1)寻找形参和变量声明(var)

(2)实参值赋值给形参

(3)找函数声明,赋值

(4)执行函数

// AO
function test(a) {
  console.log(a); // // ƒ a() {}
  var a = 1;
  console.log(a); //1
  function a() {};
  console.log(a); //1
  var b = function () {};
  console.log(b); //ƒ () {}
  function d() {}
}
test(2);

/*AO = {
  a: (1)undefined -> (2)2 -> (3)function a(){} —> (4)1
  b: (1)undefined -> (4)function(){}
  d: (3)function d() {}
};*/
//GO
var a = 1;
function a() {
  console.log(2)
}
console.log(a); //1

// GO = {
//  a: (1)undefined -> (2)function a() -> (3)1
// }
// GO AO 
function test() {
  console.log(b); //undefined
  if(a){
    var b = 2;
  }
  c = 3;
  console.log(c); //3
}

var a;
test();
a = 1;
console.log(a); //1

/*
GO = {
  a: undefined -> 1
  test: function test()
  c: 3
};

AO = {
  b: undefined
};*/

六:作用域、闭包

1.[[scope]]、作用域链

[[scope]]:

(1)函数创建时,生成的一个JS内部的隐式属性; (2)函数存储作用域链的容器,

作用域链:

(1)每个函数的作用域链都包含GO,在定义的时候都保存了GO

(2)函数在执行的前一刻(预编译的时候)生成了AO

(3)外层函数被执行的时候内层函数被定义;

(4)函数执行完成之后,AO是要销毁的,回到被定义时的状态;

function a() {
 function b() {
   function c() {}
   c();
 }
 b();
}
a();

// a定义:a.[[scope]] -> 0: GO
// a执行:a.[[scope]] -> 0: a -> AO
//                      1: GO
// b定义:b.[[scope]] -> 0: a -> AO
//                      1: GO
// b执行:b.[[scope]] -> 0: b -> AO
//                      1: a -> AO
//                      2: GO
// c定义:c.[[scope]] -> 0: b -> AO
//                      1: a -> AO
//                      2: GO
// c执行:c.[[scope]] -> 0: c -> AO
//                      1: b -> AO
//                      2: a -> AO
//                      3: GO
// c结束:c.[[scope]] -> 0: b -> AO
//                      1: a -> AO
//                      2: GO
// b结束:b.[[scope]] -> 0: a -> AO
//                      1: GO
//       c.[[scope]] X
// a结束:a.[[scope]] -> 0: GO
//       b.[[scope]] X

2. 闭包

当内部函数被返回到外部并保存时,一定会产生闭包。闭包会产生原来的作用域链不释放,过渡的闭包可能会导致内存泄漏,或加载过慢;

function test1() {
  function test2() {
    var b = 2;
    console.log(a); //1
  }
  var a = 1;
  return test2;
}
var c = 3;
var test3 = test1();
test3();

// GO = {
//  c: undefined -> 3
//  test1: function test1()
//  test3: function test2()
// }

// test1.AO = {
//  a: undefined -> 1
//  test2: function test2()
// }

// 当执行到return test2的时候test1函数已经执行完毕
// 因为test2被返回到外部,并且被全局变量test3接收。
// 这时test1.AO并没有被销毁,只是把线剪断了,test2的作用域还连着test1.AO。

// test3执行,也就是执行test2; 生成test2.AO;
// 当打印a的时候,test2.AO上面没有查找到,则向test1.AO查找。
// 再次执行test3时,实际上操作的仍然是原来test1.AO;
// test2.AO = {
//  b: undefined -> 2
// }

// 当test3执行完毕,test2.AO被销毁,但原来test1.AO仍在存在并且被test2连着不释放。

七:立即执行函数、逗号运算符

1. 立即执行函数

特点:(1):可以创建一个与外界没有任何关联的独立作用域; (2):自动执行,执行完成之后立即销毁; (3):模拟模块化。封闭作用域,向外部抛出一系列的属性和方法(或者可以window上保存属性和方法)

(function test() {
  var a = 1, b = 2;
  console.log(a + b); //3
}());

console.log(test); //Uncaught ReferenceError: test is not defined

只有表达式才可以被执行符号执行;

函数声明后面不能跟执行符号,如果括号里面有传值会被当独立的表达式。如果没传值会报错;

function test(a){
  console.log('不执行');
}(); //报语法错S yntaxError: Unexpected token ')'

function test(a){
  console.log('不执行');
}(6); //不报错也不执行 会把(6)当成独立表达式

var test = function(){}(); //🉑️
(function(){})(); //🉑️

表达式会自动忽略函数名

把函数声明变成表达式,会自动忽略函数名;

(function test(){})️(); 会忽略test;

var a = 10;
if(function b(){}){
  a+=typeof(b);
}
console.log(b); //(function b(){})当成表达式被忽略名字,报引用错误
console.log(a); //10undefined

怎么转化成表达式? + - !()&& ||

+ function test() {
  console.log(1)
}();

- function test() {
  console.log(1)
}();

! function test() {
  console.log(1)
}();

(function test() {
  console.log(1)
}());

1 && function test() {
  console.log(1)
}();

undefined || function test() {
  console.log(1)
}();

2. 逗号运算符

()里面使用逗号运算符 返回最后一位;

console.log( (6,5) ); // 5
console.log( ({}, [], function(){}) ); // ƒ (){}

八:构造函数、包装类

1. 构造函数

构造函数默认隐式返回 this ;

如果修改返回普通值的话无效果,还是返回 this;

function Car() {
  this.color = 'red';
  this.brand = 'Benz';
  
  // return this
  return '123';
}

var car = new Car();
console.log(car.color); // red

如果返回引用值的话,会覆盖 this 变成引用值;

function Car() {
  this.color = 'red';
  this.brand = 'Benz';

  // return this
  return {};
}

var car = new Car();
console.log(car); //{}
console.log(car.color); //undefined;

2. 包装类

自动创建的原始值包装对象只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法;

var a = 123;
a.len = 3;
// new Number(a).len = 3; 
// new Number(a)跟a不一样,系统没地方保存new Number(a)
// 会默认删除掉 delete new Number(a).len;
console.log(a.len); //undefined
var str = 'abc';
console.log(new String()); //里面有length属性
// new String(str) 因为new String()里面有length属性, 可以被访问
console.log(str.length);
  • arraylength 属性可以截断数组, stringlength 属性无法截断;
var arr = [1,2,3,4,5];
arr.length = 3;
console.log(arr); // [1,2,3]
arr.length = 6;
console.log(arr); // [1,2,3,4,5,empty]
var str = 'abc';
str.length = 1;
// new String(str).length = 1 
// 没地方保存new String() 会被删除 delete new String(str).length
console.log(str); //abc

3. instanceof

var obj = new Object('some text');
console.log(obj instanceof String); // true

var obj = new String('some text');
console.log(obj instanceof String); // true
console.log(obj instanceof Object); // true

九:原型、原型链

WechatIMG943.jpeg

1. __proto__constructor都是可以被修改的

function Person() {}
Person.prototype.name = '张三';
var p1 = {
  name: '李四'
};
var person = new Person();
console.log(person.name); //张三
// 修改__proto__
person.__proto__ = p1;
console.log(person.name); //李四

// 修改constructor
function PersonNew(){}
Person.prototype = {
  constructor: PersonNew
};
console.log(Person.prototype); //{constructor:  f PersonNew()}

2. 实例化之后重写prototype(直接替换一个新对象)是没用的。但是修改对象的属性是有用的。

// 重写
function Car() {}
Car.prototype.name = 'Benz';
var car = new Car();
// function Car(){
//  var this = {
//    __proto__: Car.prototype = {
//      name: 'Benz'
//    }
//  }
// }
Car.prototype = {
  name: 'Mazda'
};
console.log(car.name); // Benz



// 修改属性
function Car() {}
Car.prototype.name = 'Benz';
var car = new Car();
Car.prototype.name = 'Mazda';
console.log(car.name); // Mazda

3. 原型链的终点是 Object.prototype;

4. 子类可以修改父类的引用值属性,原始值属性不能修改

function Professor() {}
Professor.prototype.tSkill = 'JAVA';
var professor = new Professor();

function Teacher() {
  this.mSkill = 'JS';
  this.success = {
    'alibaba': 10
  }
}
Teacher.prototype = professor;
var teacher = new Teacher();

function Student() {
  this.pSkill = 'HTML/CSS'
}
Student.prototype = teacher;
var student = new Student();
// 修改引用值
student.success.baidu = '100';
console.log(teacher.success.baidu); //100
console.log(student.success.baidu); //100
// 修改原始值
student.mSkill = 'JQ';
console.log(teacher.mSkill); //JS
console.log(student.mSkill); //JQ

5. 不是所有的对象都继承于Object.prototype,比如Object.create(null);

6. __proto__系统内置的,不能自造;

var obj = Object.create(null);
obj.__proto__ = {
  count: 1
};
console.log(obj.count); //undefined

7. callee caller

callee:指向函数本身

function test() {
  console.log(arguments.callee); //f test()
}
test();

caller:调用当前函数的函数引用 (谁在调用它)

test1();
function test1() {
  test2();
}
function test2() {
  console.log(test2.caller); //f test1()
}

十:继承

1. 原型链继承

不使用默认原型,将其替换为父类的实例;

  // 父类
  function Person(name) {
    this.name = name;
    this.hobbies = ['唱歌', '跳舞'];
    this.run = function () {
      console.log(this.name + '在运动');
    }
  }

  Person.prototype.age = 10;
  Person.prototype.work = function () {
    console.log(this.name + '在工作');
  };

  // 子类
  function Web() {}

  // 核心代码
  Web.prototype = new Person();

  // 实例
  let w1 = new Web('张三');
  w1.hobbies.push('画画');
  console.log(w1.name); //undefined
  console.log(w1.age); //10
  console.log(w1.hobbies); // ['唱歌', '跳舞', '画画']
  w1.run(); //undefined在运动
  w1.work(); //undefined在工作

  let w2 = new Web('李四');
  console.log(w2.name); //undefined
  console.log(w2.age); //10
  console.log(w2.hobbies); // ['唱歌', '跳舞', '画画']
  w2.run(); //undefined在运动
  w2.work(); //undefined在工作

缺点:

  • 父类实例的属性跟原型中的值会在所有子实例中共享。包含引用值的时候会有问题;
  • 子类型在实例化时不能给父类型的构造函数传参;

2. 盗用构造函数继承

call()apply()将父类构造函数引入子类函数;

  // 父类
  function Person(name) {
    this.name = name;
    this.hobbies = ['唱歌', '跳舞'];
    this.run = function () {
      console.log(this.name + '在运动');
    }
  }

  Person.prototype.age = 10;
  Person.prototype.work = function () {
    console.log(this.name + '在工作');
  };

  // 子类
  function Web(name) {
    // 核心代码
    Person.call(this, name);
  }


  // 实例
  let w1 = new Web('张三');
  w1.hobbies.push('画画');
  console.log(w1.name); //张三
  console.log(w1.age); //undefined
  console.log(w1.hobbies); // ['唱歌', '跳舞', '画画']
  w1.run(); //张三在运动
  w1.work(); //报错 Uncaught TypeError: w1.work is not a function

  let w2 = new Web('李四');
  console.log(w2.name); //李四
  console.log(w2.age); //undefined
  console.log(w2.hobbies); // ['唱歌', '跳舞']
  w2.run(); //李四在运动
  w2.work(); //报错 Uncaught TypeError: w2.work is not a function

缺点:

  • 子类不能访问父类原型上定义的属性跟方法;

  • 每个新实例都有父类构造函数的副本,臃肿;

  • 无法实现构造函数的复用(每次用每次都要重新调用);

3. 组合继承

常用,组合原型链继承和借用构造函数继承;

  // 父类
  function Person(name) {
    this.name = name;
    this.hobbies = ['唱歌', '跳舞'];
    this.run = function () {
      console.log(this.name + '在运动');
    }
  }

  Person.prototype.age = 10;
  Person.prototype.work = function () {
    console.log(this.name + '在工作');
  };

  // 子类
  function Web(name) {
    // 核心代码
    Person.call(this, name);
  }
  // 核心代码
  Web.prototype = new Person();

  // 实例
  let w1 = new Web('张三');
  w1.hobbies.push('画画');
  console.log(w1.name); //张三
  console.log(w1.age); //10
  console.log(w1.hobbies); // ['唱歌', '跳舞', '画画']
  w1.run(); //张三在运动
  w1.work(); //张三在工作

  let w2 = new Web('李四');
  console.log(w2.name); //李四
  console.log(w2.age); //10
  console.log(w2.hobbies); // ['唱歌', '跳舞']
  w2.run(); //李四在运动
  w2.work(); //李四在工作

缺点:

  • 调用了两次父类构造函数,生成了两份实例;(子类实例将子类原型上的那份屏蔽了);

4.原型式继承

用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。

    //核心代码
    function createObj(o) {
      function F() {}
      F.prototype = o;
      return new F();
    }

    // 父类
    var person = {
      name: '111',
      friend: ['aaa', 'bbb']
    };

    var person1 = createObj(person);
    person1.name = '222';
    person1.friend.push('ccc');
    console.log(person1.name); //222
    console.log(person1.friend); //['aaa','bbb','ccc']

    var person2 = createObj(person);
    person2.name = '333';
    console.log(person2.name); //333
    console.log(person2.friend); //['aaa','bbb','ccc']

    console.log(person.name); //111
    console.log(person.friend); //['aaa','bbb','ccc']
    

缺点:

  • 所有实例都会继承原型上的属性。包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样;无法实现复用。(新实例属性都是后面添加的);

5.寄生式继承

寄生继承的思想是创建一个用于封装继承过程的函数,该函数在内部以某种方式来增强对象。最后返回对象。可以理解为在原型式继承的基础上新增一些函数或属性;

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

  var person = {
    name: '111',
    friend: ['aaa', 'bbb']
  };

  var person1 = createObj(person);
  console.log(person1.name); //111

  // 核心代码
  function createOb(o) {
    var newObj = createObj(o);
    // 增强对象
    newObj.sayName = function () {
      console.log(this.name);
    };
    // 返回对象
    return newObj
  }

  var person2 = createOb(person1);
  person2.sayName();//111

缺点:

  • 没用到原型,无法复用。

6. 寄生组合继承

(常用)子类构造函数复制父类的自身属性和方法,子类原型只接收父类的原型属性和方法。

 function Parent(name) {
    this.name = name;
    this.hobbies = ['唱歌', '跳舞'];
    this.run = function () {
      console.log(this.name + '在运动');
    }
  }

  Parent.prototype.work = function () {
    console.log(this.name + '在工作');
  };

  //web类继承Person类 原型链继承模式
  function Child(name) {
    Parent.call(this, name);
  }

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

  // Child.prototype = new Parent(); // 这里换成下面
  // 核心代码
  function prototype(child, parent) {
    var prototype = createObj(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype
  }

  prototype(Child, Parent);

  var child = new Child('张三');
  console.log(child.name); //张三

完美解决所有的问题,也不会调用两次父类构造函数;