一: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>
堵塞机制
-
script:直接使用
script标签的话,html会按照它出现的顺序依次加载&运行它。在加载&运行的过程中,会阻塞后续的DOM渲染。所以建议把script标签放在<body>元素中页面内容的后面,也就是</body>前。 为了解决script会引起页面堵塞的问题,出现了两个属性defer、async,这两个都不会堵塞页面DOM的渲染。 -
defer:只适用于外部脚本。如果
script标签使用了defer属性,就代表告诉浏览器异步的立即下载但延迟执行。不会影响后续DOM的渲染。脚本会被延迟到整个页面都解析完毕后在运行。会在DOMContentLoaded事件之前执行。能保证执行顺序(如果设置了多个,会按照顺序执行)。⚠️ 在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在
DOMContentLoaded事件触发前执 行,因此最好只包含一个延迟脚本。 -
async: 只适用于外部脚本。如果
script标签使用了async属性,浏览器会立即下载相应的脚本,在下载的过程中页面的处理不会停止,下载完成后立即执行,执行过程中页面处理会停止。会在页面的load事件前执行,有可能会在DOMContentLoaded之前或之后。不能保证执行顺序。适合没有依赖关系的脚本。
defer与async的区别是: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标签,输出的结果是:
如果使用async属性,输出的结果是:
上面的测试可以证明: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):比较运算符
number跟string比较:string会先转成数字进行比较;string跟string比较: 会比较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..:用于枚举对象中的非符号键属性,包括继承的可枚举属性;不能保证对象属性的返回顺序。不能用来遍历map、set在谷歌下,先按数值升序,在按插入字符串的顺序遍历的。符号类型的属性会被忽略。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..:用于可迭代对象(包括Array,Map,Set,String,TypedArray,arguments) 对象等等)。按照顺序迭代元素;
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
undefined、nullundefined、null没有toString方法,会报错;其他的原始值都有toString方法。但不是使用的Object.prototype原型的toString方法;都进行了重写;
var a = 1;
Object.prototype.toString.call(a); // '[object Number]'
Number.prototype.toString.call(a); // '1'
undefined、null 既不大于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);
array的length属性可以截断数组,string的length属性无法截断;
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
九:原型、原型链
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); //张三
完美解决所有的问题,也不会调用两次父类构造函数;