轻笑Chuckle
外观

系列 · JavaScript 基础 学习笔记

JavaScript基础笔记(3)

函数与对象,作用域、this、闭包、JSON、正则、原型

28 分钟 11,210 字 次浏览 条评论

JS函数简介

使用function关键字定义函数对象,函数的typeof类型也是function

function hello(){
console.log('hello');
}
console.log(typeof hello)//function
console.log(fun instanceof Object)//true

使用函数表达式创建匿名函数,即将匿名函数赋值给一个变量

var fun = function(){
console.log("匿名函数");
};
fun();//匿名函数

函数是对象,也可以new出来,但一般不用这种形式,Function里面的参数都必须是字符串格式

var fun = new Function('a', 'b', 'console.log(a + b);');
fun(1,2); // 3

使用return终止函数或返回一些数据,默认返回undefined

函数名就是整个函数,JS加载的时候,只加载函数名,不加载函数体

function fun(){
console.log("hello");
};
console.log(fun);//输出fun(){console.log("hello");}
console.log(fun());//先执行函数,再输出返回值

函数的调用

**1、调用函数可以函数名调用,可以用call()**方法

function fun(){ console.log('hello'); }
fun(); // hello
fun.call(); // hello

2、在对象中保存多个函数,通过对象的方法来调用,函数作为对象的属性保存,这个函数就是对象的方法

var obj = {
fun1: function(){
console.log("函数一");
},
fun2: function(){
console.log("函数二");
}
}
obj.fun1();// 函数一
obj.fun2();// 函数二

3、立即执行函数,在定义后立即执行

(function() {
console.log('立即执行函数');
})();
//立即执行函数

4、通过构造函数调用

function Fun() {
console.log("这是一个函数");
}
new Fun();// 这是一个函数

5、绑定事件函数,触发事件后立即执行的函数

var btn = document.getElementById('btn');
//绑定事件
btn.onclick = function() {
console.log('点击按钮后,要做的事情');
};

6、定时函数

setInterval(function () {
console.log("hello");
}, 1000);//每1000ms执行一次

形参和实参

**形参:**函数在定义时要求传入的一些参数 **实参:**调用函数时传入的一些参数

实参将按顺序传递给函数中对应的形参

function fun(a, b){//该函数需要两个形参
console.log(a + b);
};
//调用时传入两个实参
fun(1, 2)// 3

实参和形参,数量可以不同,调用函数时,解析器不会检查实参的数量。

**实参多余形参:**忽略多余的实参 **实参少余形参:**没有接收到值的形参默认为undefined,undefined参与运算为NaN

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

调用函数时,解析器也不会检查实参类型,实参可以是任意数据类型

类数组对象arguments

在调用函数时,浏览器每次都会传递进两个隐含的参数:(箭头函数没有自己的this和arguments。)

  1. 函数的上下文对象 this
  2. 封装实参的对象 arguments

arguments是一个类数组对象(伪数组),不是一个真正的数组,除了length属性、可以通过索引获取元素之外没有任何数组属性和方法。

Array.from()能将伪数组转为真数组

function fun(a, b){
console.log(arguments);// Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
console.log(Array.from(arguments));// [1, 2]
console.log(typeof arguments);// object,arguments 的原型是 Object,而数组的原型是 Array
return a + b;
};
fun(1, 2);

**案例:**将传入的实参进行求和,无论实参的个数有多少:

function fun() {
var arr = Array.from(arguments);
var result = arr.reduce((prev, item) => {
return prev + item;// 累计求和
}, 0);
return result;
}
var sum = fun(0, 1, 2, 3, 4, 5, 6);
console.log(sum); // 21

arguments.callee返回正在执行的函数

function fun() {
console.log(arguments.callee);
}
fun();// 返回了fun这个函数对象, fun() {console.log(arguments.callee);}

通过arguments修改传入的实参

function fun(a, b){
//修改传入的最后一个参数为0
arguments[arguments.length-1] = 0;
return a + b;
};
console.log(fun(1, 2));// 1

作用域

变量或函数的作用范围。作用域在函数定义时,就确定。 全局作用域:作用于整个script标签内,或作用于一个独立的JS文件 函数作用域:作用于函数部 块级作用域:ES6新增,作用于一个区块{}内

在全局作用域中有一个全局对象 window(和微软没有半毛钱关系),它代表的是浏览器的窗口,由浏览器创建。

创建的全局变量都会作为 window 对象的属性保存。 创建的全局函数都会作为 window 对象的方法保存。

变量如果未经声明(没有var、let、const)就赋值,是全局变量

var a = 1;
(function() {
var [a, b] = [0, 0];
console.log(a);// 0
console.log(window.a);// 1,访问全局变量a
})();
console.log(b);// 报错Uncaught ReferenceError:b is not defined

连续赋值时,除了第一个声明的变量之外,后面的变量都是全局变量

function fun(){
var a = b = 1; // 连续赋值,a是函数作用域变量,b是全局变量
}
fun();
console.log(a);// 报错Uncaught ReferenceError: a is not defined
console.log(b);// 1

变量、函数提升

**全局作用域的预处理:**JS在解析代码之前,将当前 JS 代码中所有变量的定义和函数的定义,放到所有代码的最前面,先进行声明但不进行赋值,默认都是undefined。

任何变量,如果未经声明(没有var、let、const)就赋值,此变量是属于 window 的属性,而且不会做变量提升

console.log(a);//undefined
var a = 1;
console.log(b);//报错Uncaught ReferenceError:b is not defined
b = 1;//赋值但没定义,相当于window.b,不进行变量提升
console.log(b);//1

其它作用域也有类似的预处理,这就是变量提升,即将变量声明提升到它所在作用域的最开始的部分。

函数的声明提前(函数提升): 使用函数声明的形式创建的函数function fun(){},会被函数提升,可以提前调用函数,能正常执行。 使用函数表达式创建的函数var fun = function(){},不会被函数提升,但属于变量提升,不能提前调用,会被认为不是一个函数。

函数的形参就相当于在函数作用域中声明了变量。

console.log(fun1(1, 2));// 3,提前调用可以正常执行函数
function fun1(a, b){ return a + b; }
fun2();//报错Uncaught TypeError: fun2 is not a function
var fun2 = function(){}

函数提升优先于变量提升

fun(); // 先函数提升,所以输出B
// 函数提升
function fun() {
console.log('B');
}
console.log(fun);// ƒ fun() {console.log('B');}
// 变量提升,此时相当于重新声明fun变量
var fun = function () {
console.log('A');
};
fun(); // A

函数的嵌套:会出现作用域链,在嵌套函数中,变量会从内到外逐层寻找它的定义(查找时,采用就近原则)。

var a = 0;
function fn() {
// 外部函数
fun();//函数提升,提前调用也正常执行
function fun() {
// 内部函数
console.log(a);//先找到var a = 1
//输出undefined,因为实际代码中变量的调用在变量声明之前,属于变量提升,默认值为undefined
}
var a = 1;
//fun()若内部函数在此执行,则会输出 1
}
fn();

this指向

JavaScript的this原理--阮一峰

this 即‘当前’,指的是函数运行时所在的环境,它永远指向函数的真实调用者,如果没有调用者,就指向全局对象window。

var a = -1;
function foo(){
console.log(this.a);
};
var obj1 = {
a:1,
foo:foo
};
var obj2 = {
a:0,
foo:foo
};
obj1.foo();// 1
obj2.foo();// 0
foo();// -1
window.foo();// -1

this在函数调用时绑定,函数执行时会创建一个活动记录,这个记录里包含了该函数中定义的参数,也包含函数在哪里被调用(调用栈),this就是其中的一个属性。

绑定规则

  1. 默认绑定:把this绑定到全局对象window,以函数的形式而非对象的方法(包括普通函数、定时器函数、立即执行函数)调用时。
  2. **对象中的this:隐式绑定(上下文绑定)**对象内部方法的this指向调用此方法的对象,谁调用就指向谁
  3. **构造函数中的this:**构造函数中的this指向构造函数下创建的实例对象,构造函数返回创建的对象。
  4. 以事件绑定函数的形式调用时,this指向绑定事件的对象
  5. **箭头函数中的this:**指向函数作用域所用的对象

1、默认绑定:把this绑定到全局对象window,以函数的形式而非对象的方法(包括普通函数、定时器函数、立即执行函数)调用时。

var a = 0;
function fun() {
this.a = 0;
console.log(this);// 输出this指的是谁
console.log(this.a);
this.a++;
console.log(this.a);
}
fun();//输出 Window、0、1
window.fun();//输出 Window、0、1
//fun代码块中,this指的都是window,而a变量是全局变量,即是window的属性,所以this.a访问的是window.a

**2、对象中的this:隐式绑定(上下文绑定)**对象内部方法的this指向调用此方法的对象,谁调用就指向谁

var a = 0;
function fun() {
this.a = 0;// 先把对象中的a值改为0
console.log(this);// 输出this指的是谁
console.log(this.a);
this.a++;
console.log(this.a);
}
var obj = {
a: 1,
//注意是函数名,因为函数名就代表了这个函数
fun: fun//将fun绑定给obj对象的fun属性,此时fun的this指向obj这个真实调用者
}
obj.fun();//输出 obj对象{a: 1, fun: ƒ}、0、1

多层对象时,内部方法this指向离被调用函数最近的对象

var a = 0;
function fun() {
console.log(this);// 输出this指的是谁
console.log(this.a);
this.a++;
console.log(this.a);
}
var obj1 = {
a: 1,
//注意是函数名,因为函数名就代表了这个函数
fun: fun,//将fun绑定给obj1对象的fun属性
obj2: {
a: 2,
fun: fun
}
}
obj1.fun();//输出 obj1对象{a: 0, obj2: {…}, fun: ƒ}、1,2
obj1.obj2.fun();//输出 obj2对象{a: 0, fun: ƒ}、2,3

上面代码中,若obj2中没有a属性,this.a是undefined,即无论对象嵌套多少层,this只会指向直接调用该函数的对象(离被调用函数最近的对象)

this永远指向函数的真实调用者

var a = 0;
function fun() {
this.a = 2;
this.foo = fun2;
}
function fun2() {
console.log(this.a);
}
var obj = {
a: 1,
fun: new fun().foo
}
obj.fun();// 1,多次引用的传递,实际上是obj调用了fun2

**隐式绑定的丢失:**通过赋值,导致隐式绑定的丢失。

var a = 0;
function fun() {
console.log(this.a);
}
var obj = {
a: 1,
fun: fun
}
obj.fun();// 1
var foo = obj.fun;
foo();// 0

上面代码中,obj的fun属性引用了fun函数的引用内存地址,在obj.fun()调用函数时,fun函数中的this会动态绑定对象(当前函数的直接调用者,即obj对象),将obj.fun的引用地址赋值给了foo那么foo也引用了fun函数的引用内存地址,使用foo()时,fun函数中的this也会动态绑定对象(当前函数的直接调用者,即window对象)。

**3、构造函数中的this:**构造函数中的this指向构造函数下创建的实例对象,构造函数返回创建的对象。

function Fun(name, age){
this.name = name;// 让构造函数创建的实例对象中的name属性等于传入的实参name
this.age = age;
}
var fun = new Fun('chuckle', 19);
console.log(fun);//Fun {name: 'chuckle', age: 19}
console.log(typeof fun);// object构造函数返回创建的对象

4、以事件绑定函数的形式调用时,this指向绑定事件的对象

var btn = document.getElementById('btn');
//绑定事件
btn.onclick = function() {
console.log(this);//this指向带有btn id的整个标签
//<button id="btn"></button>
};

**5、箭头函数中的this:**指向函数作用域所用的对象

箭头函数的重要特征:箭头函数没有自己的this和arguments,但它会继承自己定义时所处的外层执行环境的this指向,指向当前定义时所在的对象,call()、apply()、bind()等方法无法改变箭头函数继承的this指向。

简单地说,箭头函数会找它的上一级作用域。如果父级作用域还是箭头函数,就再往上找,一层层找,直到找到this指向的对象

var obj = {
a: 1,
fun: ()=>{
console.log(this);
}
}
//obj是一个全局变量,fun是一个箭头函数,在定义时指向这个全局变量的作用域,即window对象
obj.fun();//Window {window: Window, self: Window, document: document, name: '', location: Location, …}

多层对象时,仍然指向最外部对象定义时所在的环境

var obj1 = {
a: 1,
obj2: {
a: 2,
fun: ()=>{
console.log(this);
}
}
}
//obj2是obj1的属性,obj是一个全局变量,所以定义时箭头函数this仍然指向window对象
obj1.obj2.fun();//Window {window: Window, self: Window, document: document, name: '', location: Location, …}

用构造函数创建对象,创建出来的对象中的箭头函数和普通函数,都指向构造函数创建出来的对象,但一个是在定义时指向obj对象,一个是在调用时指向obj对象

function foo(){
console.log(this);
}
function Fun(){
this.fun = ()=>{
console.log(this);
};
this.foo = foo;
}
var obj = new Fun();
obj.fun();//Fun {fun: ƒ, foo: ƒ}
obj.foo();//Fun {fun: ƒ, foo: ƒ}
console.log(typeof obj);// object

普通函数中的箭头函数:

箭头函数的外层如果有普通函数,那么箭头函数的this就是这个外层的普通函数的this,箭头函数的外层如果没有普通函数,那么箭头函数的this就是全局变量window。

或者说,普通函数中的箭头函数的this被绑定到该函数执行的作用域上

var obj={
birth:1990,
getAge:function(){
//对象内部方法的this指向调用此方法的对象
var b = this.birth;// 1990
//箭头函数的this就是这个外层的普通函数的this,所以在定义时就指向obj
var fn = ()=>2023-this.birth;
return fn();
}
};
obj.getAge();//33
var birth = 2000
var obj={
birth:1990,
getAge:function(){
fn = ()=>2023-this.birth;
return fn();
}
};
obj.getAge();//33
var foo = obj.getAge;//发生隐式绑定的丢失,函数的作用域从obj对象变为全局window
foo();// 23,

改变this指向的方法

JS在Function的porpertype属性上提供了3个方法来强行修改函数内部的this指向,不想改变this指向则传入null或this,这三个方法都不会改变原函数的指向(动态的)

  1. call():传入多个参数,第一个是要修改的this的指向,剩下的会传给函数当参数,然后会执行这个函数
  2. apply():传入两个参数,第一个是要修改的this的指向,第二个是一个数组,它保存了要传入函数的多个参数,然后会执行这个函数
  3. bind():传入多个参数,第一个是要修改的this的指向,剩下的会传给函数当参数,不会执行这个函数,但会返回指定this和指定实参的原函数拷贝(一个改变了this指向和已经传入了参数的新函数)

**1、call():**传入多个参数,第一个是要修改的this的指向,剩下的会传给函数当参数,然后会执行这个函数

fun.call(想要将this指向的对象, 函数实参1, 函数实参2);

通过call()调用函数,不改变指向

var a = 1;
function fun() {
console.log(this);// window
console.log(this.a);
}
//将this的指向又传给this,指向没有被改变
fun.call(this); // 1
fun(); // 1
fun.call(); // 1
fun.call(window); // 1
fun.call(null); // 1

通过call()改变this指向

var a = 1;
var obj = {
a: 0
}
function fun() {
console.log(this);
console.log(this.a);
}
fun(); // window 1
fun.call(obj); // obj 0

**2、apply():**传入两个参数,第一个是要修改的this的指向,第二个是一个数组,它保存了要传入函数的多个参数,然后会执行这个函数

fun.apply(想要将this指向的对象, [函数实参1, 函数实参2]);
var obj = {
name: 'chuckle',
age: 19,
};
function fun(name) {
console.log(this);// 输出obj对象{name: 'chuckle', age: 19}
console.log(this.name);//chuckle
this.name = name;//改变obj的name属性为传入的实参
console.log(this.name);//qx
}
fun.apply(obj, ['qx']);//传一个实参,也需要传数组
console.log(obj);//{name: 'qx', age: 19},name被改变

通过apply()求数组的最大值:

数组本身没有求最大值的方法,但是数学对象中有Math.max(数字1,数字2...),apply可以传入一个数组作为其参数,不改变其指向即可

var arr = [4, 2, 6, 5];
var maxValue = Math.max.apply(this, arr);// 不改变执行,传入一个数组作为参数
console.log(maxValue);// 6

3、bind():传入多个参数,第一个是要修改的this的指向,剩下的会传给函数当参数,不会执行这个函数,但会返回指定this和指定实参的原函数拷贝

var a = 1;
var obj = {
a: 0
}
function fun(a) {
console.log(this.a + a);
}
var result = fun.bind(obj, 10); //返回指定this和指定实参的原函数拷贝
console.log(typeof result);// function
result(10);// 10
result(20);// 10
var result = fun.bind(obj);
result(10);// 10
result(20);// 20

内存回收机制

JS具有内存自动回收机制,周期性的找出不再继续使用的变量,然后释放其占用的内存。

在闭包中,如果引用了外部的变量,则无法进行释放和回收,造成内存泄漏

**常见内存泄漏:**全局变量、闭包、Dom元素的引用、定时器

IE回收不了闭包里面引用的变量,但2023年了,主流浏览器都能回收闭包内不再使用的变量,js闭包测试--司徒正美

for(let i = 0; i < 5; i++) {
//for循环中是块级作用域,每执行一次循环,j就会被自动回收,所以不会报错(const 不能重新赋值)
const j = '局部变量'
console.log(j);
}

闭包

如果外部作用域有权访问另外一个函数内部的局部变量时,那就产生了闭包。这个内部函数称之为闭包函数

函数和函数内部能访问到的变量的总和,就是一个闭包。

闭包是JS函数作用域的副产品,因为JS的函数内部可以使用函数外部的变量。

闭包的生命周期: **产生:**内部函数被声明时就产生了。 **死亡:**嵌套的内部函数成为垃圾对象时。(比如fun = null,就可以让 fun 成为垃圾对象)

**闭包的作用:**将函数内部的变量(局部变量)能被外部访问,隐藏一些变量,延长局部变量的生命周期

function fun1() {
let a = 10;
return function fun2() {
console.log(a);
};
}
//获取到一个能访问fun1内部的变量的函数
var result = fun1();
//在fun1函数的外部,执行了内部函数fun2,并访问到了fun1的内部变量a
result();// 10

上面的代码中,通过一些操作,让外部作用域(即全局作用域)有权访问函数fun1中的局部变量,在fun1中就产生了闭包,函数fun1是闭包函数,闭包是fun2和fun2所能访问到的变量a。

延长局部变量的生命周期:

// fun1执行完一次,局部变量就立即销毁,下次调用a还是0;
function fun1() {
let a = 0;
a++;
console.log(a);
}
fun1();// 1
fun1();// 1
// 由于产生了闭包,fun2函数还要继续调用变量a,所以fun1函数中的变量a不会立即销毁,仍然保留在内存中。
// 只有等所有函数把变量a调用完了,变量a才会销毁。
function fun1() {
let a = 0;
function fun2() {
a++;
console.log(a);
};
return fun2;
}
var result = fun1();
result();// 1
result();// 2

隐藏一些变量:

打游戏时通常有血条和蓝条,我们当然不希望用户window.blood就能修改血量。将血量变量blood放进匿名函数中,在函数内部去声明一些修改血量的代码再赋给全局变量,就能在匿名函数外部通过这些函数去修改血量,但不能直接访问血量。

!function(){ //匿名函数
let blood = 10;// 血量
// 扣若干滴血,默认扣1
window.reduceBlood = (a)=>{
a ? blood -= a : blood -= 1;
checkBlood();
};
// 增加若干滴血,默认加1
window.increaseBlood = (a)=>{
a ? blood += a : blood += 1;
checkBlood();
};
// 查询血量
window.checkBlood = ()=>{
console.log(`血量还有${blood}`);
};
}();
// 先连续扣血
reduceBlood();// 血量还有9
reduceBlood(5);// 血量还有4
//再加血
increaseBlood();// 血量还有5
increaseBlood(5);// 血量还有10
//查询血量
checkBlood(); // 血量还有10

**封装JS模块:**定义具有特定功能的JS模块,将所有的数据和功能都封装在一个函数内部,只向外暴露指定的对象或方法。模块的调用者,只能调用模块暴露的对象或方法来实现对应的功能

如果不想将这三个函数直接赋给全局变量,也可以让函数返回一个装有三个方法的对象

function Blood(){ //匿名函数
let blood = 10;// 血量
// 扣若干滴血,默认扣1
const reduceBlood = (a)=>{
a ? blood -= a : blood -= 1;
checkBlood();
};
// 增加若干滴血,默认加1
const increaseBlood = (a)=>{
a ? blood += a : blood += 1;
checkBlood();
};
// 查询血量
const checkBlood = ()=>{
console.log(`血量还有${blood}`);
};
// 返回一个对象,包含三个方法
return {
reduceBlood: reduceBlood,
increaseBlood: increaseBlood,
checkBlood: checkBlood
}
};
var figure1 = new Blood();
var figure2 = new Blood();
figure1.checkBlood(); // 血量还有10
figure2.checkBlood(); // 血量还有10
//可以看到,figure1和figure2是独立的,两个闭包函数是互不影响
//减少1的血量不会减少2,血量不会互相干扰
figure1.reduceBlood(5);// 血量还有5
figure2.reduceBlood();// 血量还有9
// 人物死亡需要重新创建角色,记得回收闭包
// figure1 = null;

面向对象概述

Java中已经学习过了什么是面向对象,这里不再扯概念。

JS是基于原型的面向对象,JS中的对象(Object)是依靠构造器(constructor)和原型(prototype)构造出来的

在ES6中,新引入了类(Class)和继承(Extends)来实现面向对象

**面向对象的编程思想:**对代码和数据进行封装,并以对象调用的方式,对外提供统一的调用接口

调用对象的属性:obj.name或obj['name']

对象的创建

创建对象:

1、对象字面量{}

var obj = {
name: 'chuckle',
age: 19,
obj2: {
a: 1,
b: 2
},
foo: function(){
console.log(`我的名字${this.name}`);
}
}
obj.foo();// 我的名字chuckle
console.log(obj.name);// chuckle
console.log(obj['name']);// chuckle

2、工厂模式 new Object() 大量创建同种对象

function createPerson(name, age) {
//创建一个新的对象
var obj = new Object();
//向对象中添加属性
obj.name = name;
obj.age = age;
return obj;// 返回这个对象的引用
}
var obj1 = createPerson('chuckle', 19);// {name: 'chuckle', age: 19}
var obj2 = createPerson('qx', 18);// {name: 'qx', age: 18}
// 所以创建的对象都是 Object 这个类型
console.log(typeof obj1);// object
console.log(typeof obj2);// object

3、构造函数

function Person(name, age) {
//构造函数中this指的是当前对象实例
this.name = name;
this.age = age;
this.foo = function () {
console.log(this.name);
};
}
var p1 = new Person('chuckle', 19);
console.log(p1);// Person {name: 'chuckle', age: 19, foo: ƒ}
console.log(typeof p1);// object
p1.foo();// chuckle

构造函数

**构造函数:**是一种特殊的函数,主要用来创建和初始化对象,也就是为对象的成员变量赋初始值。

创建构造函数时,里面的属性和方法前必须加this,this就表示当前要构造的对象。

普通函数是直接调用,而构造函数需要使用 new 关键字来调用。

构造函数的执行流程:

  1. 立刻创建一个对象
  2. 将新建的对象设置为函数中this,使得在构造函数中可以使用this来引用新建的对象
  3. 遂行执行函数中的代码,给这个新对象添加属性和方法
  4. 将新建的对象作为返回值返回(构造函数中无需return)
// 构造函数
function Student(name) {
this.name = name; //this指的是构造函数中的对象实例
}
var stu = new Student('chuckle');// Student {name: 'chuckle'}
//上面的代码相当于:
function Student(name) {
var obj = new Object();
obj.name = name;
return obj;
}
var stu = Student('chuckle');// {name: 'chuckle'}

静态成员和实例成员: 1、静态成员:构造函数本身上添加的成员,静态成员只能通过构造函数访问,不能通过对象访问 2、实例成员:构造函数内部通过this添加的成员,实例成员只能通过实例化的对象进行访问

function Student(name) {
this.name = name; //this指的是构造函数中的对象实例
}
Student.sName = 'qx';// 添加静态成员
var stu = new Student('chuckle');// 通过构造函数创建实例对象
// 实例成员只能通过实例化的对象进行访问
console.log(stu.name);// chuckle
console.log(Student.name);// Student,函数的name是它的函数名
// 静态成员只能通过构造函数访问,不能通过对象访问
console.log(stu.sName);// undefined
console.log(Student.sName);// qx

类、实例 使用同一个构造函数创建的对象,都称为一类对象,也将构造函数称为类。通过一个构造函数创建的对象,称为该类的实例。

使用 instanceof 可以检查一个对象是否为一个类的实例。

function Person() {}
function Dog() {}
var person = new Person();
var dog = new Dog();
console.log(person instanceof Person); // true
console.log(dog instanceof Person); // false
//所有的对象都是Object的实例,所有类都是Object的子类。
console.log(person instanceof Object); // true

对象的基本操作

  1. 向对象中添加属性:对象.属性名 = 属性值
  2. 获取对象中的属性:对象.属性名
  3. 修改对象的属性值:对象.属性名 = 新值
  4. 删除对象的属性:delete 对象.属性名
  5. in 运算符:属性名 in 对象检查一个对象中是否含有指定的属性
var obj = {};// 创建
obj.name = 'chuckle';// 添加
console.log(obj.name);// chuckle
obj.name = 'qx';// 修改
console.log(obj.name);// qx
delete obj.name;// 删除
console.log('name' in obj);// false

遍历对象

遍历对象时,要根据对象的结构配合使用多种方法,通常还需要配合数组的遍历方法

1、for-in遍历对象的属性,再使用对象[属性名]获取属性值

for-of用于遍历数组的元素,直接获取元素。

var obj = {
student: [
{ name: "chuckle", age: 19 },
{ name: "qx", age: 18 },
],
teacher: [
{ name: "张三", age: 37 },
{ name: "李四", age: 35 }
]
};
for(let i in obj){
//第一层循环获取student和teacher两个属性
console.log(i);//输出属性名
//obj[i]获取属性值,是数组,遍历数组使用for-of,虽然用for-in也行,这里都作展示
for(let j of obj[i]){
console.log(`${j.name}:${j.age}`);
}
// for(let j in obj[i]){
// console.log(`${obj[i][j].name}:${obj[i][j].age}`);
// }
}
student
chuckle:19
qx:18
teacher
张三:37
李四:35

2、Object.keys返回对象自身属性名组成的数组,Object.values返回对象自身属性值组成的数组

var obj = {
student: [
{ name: "chuckle", age: 19 },
{ name: "qx", age: 18 },
],
teacher: [
{ name: "张三", age: 37 },
{ name: "李四", age: 35 }
]
};
Object.keys(obj).forEach((key) => {
console.log(key);//输出属性名
obj[key].forEach((key) => {
console.log(`${key.name}:${key.age}`);
})
})
Object.values(obj).forEach((value) => {
value.forEach((key) => {
console.log(`${key.name}:${key.age}`);
})
})
student
chuckle:19
qx:18
teacher
张三:37
李四:35
chuckle:19
qx:18
张三:37
李四:35

3、Object.entries()返回Object.keys与Object.values的结合体,一个嵌套的数组,数组内包括了属性名与属性值,下标0存储属性名

var obj = {
student: [
{ name: "chuckle", age: 19 },
{ name: "qx", age: 18 },
],
teacher: [
{ name: "张三", age: 37 },
{ name: "李四", age: 35 }
]
};
Object.entries(obj).forEach((item)=>{
console.log(item[0]);//输出属性名
//属性值仍是数组,接着遍历,数组中存着对象,直接去访问对象的属性拿到属性值输出
item[1].forEach((item)=>{
console.log(`${item.name}:${item.age}`);
})
})
student
chuckle:19
qx:18
teacher
张三:37
李四:35

4、Object.getOwnPropertyNames()与Object.keys差不多,不同的是会返回对象的所有属性,包括了不可枚举属性,如数组对象的length

5、Object.getOwnPropertySymbols()返回对象内的所有Symbol属性的数组,对象初始化的时候,内部不包含任何Symbol属性

对象访问器

JS提供了Getter(get关键字)和 Setter(set关键字) 来定义对象访问器(属性访问器)

var obj = {
name: 'chuckle',
age: 19,
get getAge() {
return this.age;
},
set setAge(age) {
this.age = age;
}
}
console.log(obj.getAge);// 19
obj.setAge = 20;
console.log(obj.getAge);// 20

使用 getter 和 setter 可以确保更好的数据质量,一些会随时间而变等的属性(如年龄),实际无需静态地存储在对象中,且可以对数据进行加工处理,类似数据库中的视图的功能

var obj = {
name: 'chuckle',
birthYear: 2003,
get age() {
//年龄不适合直接存储在对象中,我们可以存储生日,而年龄属性则作为一个getter进行处理后再返回
return new Date().getFullYear() - this.birthYear;
},
//如果去修改年龄,实际上应该修改birth出生年份
set age(age) {
this.birthYear = new Date().getFullYear() - age;
}
}
console.log(obj.age);// 20,对象中实际上没有age
obj.age = 19;
console.log(obj.age);// 19
console.log(obj.birthYear);// 2004

Object对象

JavaScript中的对象其实就是一组数据和功能的集合。

Object对象是所有对象的祖宗,其他对象都继承自Object,即其它对象都是Object的实例

每个Object类型的实例共有的属性和实例方法(定义在Object原型对象Object.prototype上的方法。可以被Object实例直接使用):

  1. **constructor:**保存用于创建当前对象的构造函数。
  2. **__proto__:**隐式原型,指向的Object原型对象(父对象,所有类型的对象都有这个属性)
  3. **hasOwnProperty():**检测实例中是否有指定属性。
  4. **isPrototypeOf():**判断传入的对象是否是当前对象的原型
  5. **propertyIsEnumerble():**检查指定属性能否使用for-in来枚举遍历
  6. **toLocaleString():**返回对象的字符串表示
  7. **toString():**返回对象的字符串表示
  8. **valueOf():**返回对象本身

**静态方法:**直接定义在Object对象的方法,Object.直接调用

控制对象状态的方法:

  1. **preventExtensions():**防止对象扩展
  2. **isExtensible():**判断对象是否可扩展
  3. **seal():**禁止对象配置
  4. **isSealed():**判断一个对象是否可配置
  5. **freeze():**冻结一个对象
  6. **isFrozen():**判断一个对象是否被冻结

对象属性模型的相关方法

  1. **keys():**返回对象自身属性名组成的数组
  2. **getOwnPropertyNames():**与keys()差不多,但返回对象所有属性的数组,包括了不可枚举属性,如数组对象的length
  3. **getOwnPropertyDescriptor():**获取某个属性的描述对象,参数(对象,属性名的字符串)
  4. **defineProperty():**通过描述对象,定义或修改某个属性。给对象添加一个属性并指定该属性的配置
  5. **defineProperties():**通过描述对象,定义多个属性。
  6. **hasOwn():**判断是否为自身的属性

原型链相关方法

  1. **is():**比较两个值是否严格相等,严格比较
  2. **create():**指定原型对象和属性,返回一个新的对象
  3. **values():**返回对象自身属性值组成的数组
  4. **entries():**返回一个数组,元素是对象自身的(不含继承的)所有可遍历属性的键值对数组
  5. **fromEntries():**将一个键值对数组转为对象。
  6. **assign():**对象的合并,复制一个或者多个对象来创建一个对象,浅拷贝,将源对象的所有可枚举的自身属性,复制到目标对象
  7. **getPrototypeOf():**获取对象的原型对象(Prototype对象)
  8. **setPrototypeOf():**设置对象的原型对象

Object.keys(obj).length获取对象的长度

Object.prototype.toString.call()可以在任意值(对象)上调用这个方法,以判断这个值的类型

//[object Number]第一个值代表是对象,第二个值表示该值的构造函数即类型
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"

JSON

JSON(JavaScript Object Notation),即JavaScript对象表示法,它是一种数据交换的文本格式,使用JS语法来描述数据对象,而不是一种编程语言。

大多数语言都支持对json的解析。JS中可以原生地把json转为object对象。

**数据结构:**Object、Array **基本类型:**string,number,true,false,null(json无法表示undefined)

{}//这是一个json
null//也可是一个json
1//也可是一个json
//下面也是一个json
[
{},
{}
]
{
"name":"chuckle",
"age":"19"
}
//一个常见的json
{
"student": [
{ "name": "chuckle", "age": 19 },
{ "name": "qx", "age": 18 },
],
"teacher": [
{ "name": "张三", "age": 37 },
{ "name": "李四", "age": 35 }
]
}

对象和 json 没有长度,json.length 的打印结果是 undefined

使用for-in遍历json:

//将json存在变量中
var json = {
"student": [
{ "name": "chuckle", "age": 19 },
{ "name": "qx", "age": 18 },
],
"teacher": [
{ "name": "张三", "age": 37 },
{ "name": "李四", "age": 35 }
]
};
for (let i in json) {
console.log(i);
for(let j in json[i]){
console.log(`${json[i][j].name}:${json[i][j].age}`);
}
}
student
chuckle:19
qx:18
teacher
张三:37
李四:35

for-in获取json对象的属性:

var json = { "name":"chuckle", "age":19 };
for (i in json) {
console.log(i);
}
// name
// age

for-in获取json对象的属性的值:

var json = { "name":"chuckle", "age":19 };
for (i in json) {
console.log(json[i]);
}
// chuckle
// 19

前端收到的api通常也是json格式,如一言api:

{
"code": 200,
"type": "一言",
"content": "总觉得跟你在一起,不管多高的地方都可以到达。"
}

JSON.parse() 将数据转换为 JavaScript 对象 JSON.stringify() 将 JavaScript 对象转换为字符串

Map对象

Map对象保存键值对,元素会保持其插入时的顺序。

Map的键可以是任意数据类型,包括函数、对象或任意基本类型。

在需要进行很多新增操作,且需要储存许多数据的时候,使用 Map 会更高效

var m = new Map();
//或传入一个嵌套数组
m = new Map([
['x', 1],
['y', 2]
]);

Object与Map增删改查基本操作。

var o = {};
var m = new Map();
//添加
o.x = 1;
m.set('x', 1);
//修改
o.x = 2;
m.set('x', 2);
//递增
o.x++;
m.set('x', m.get('x')+1);
//获取
o.x;
m.get('x');
//删除
delete o.x;
map.delete('x');

Map的键值对个数可以通过size属性获取

var m = new Map();
m.set('x', 1);
m.set('y', 2);
console.log(m);//Map(2) {'x' => 1, 'y' => 2}
console.log(m.size);//2

Map的方法

基本方法:

  1. **get():**获取元素
  2. **set():**设置元素
  3. **has():**检查是否有指定key
  4. **clear():**清空map
  5. **delete():**删除指定元素

遍历方法:

  1. keys():提取键并返回键的迭代器MapIterator对象
  2. values():提取值并返回值的迭代器MapIterator对象
  3. entries():提取键值对并返回取键值对的迭代器MapIterator对象
  4. **forEach():**传入回调函数(value, key)=>{}
var m = new Map([
['x', 1],
['y', 2]
]);
console.log(m.keys());//MapIterator {'x', 'y'}
console.log(m.values());// MapIterator {1, 2}
console.log(m.entries());// MapIterator {'x' => 1, 'y' => 2}
//迭代器可以用for-of遍历
for (let [key, value] of m.entries()) {
console.log(key, value);//x 1, y 2
}
m.forEach((value, key) => {
console.log(key, value);//x 1, y 2
})

Set对象

Set是唯一值的集合,与map类似,map存放的是键值对,而set只存放唯一值。

创建set对象:

var s = new Set();
var s = new Set([1,2,3]);

set对象与数组也很像,可以互相转换

var arr1 = [1, 2, 3];
var s = new Set(arr1);//数组转为set对象,会去重
var arr2 = [...s];//set对象转为数组

可利用set值唯一的特性做数组去重、并集、交集、差集操作

//1、去重
var s = new Set([1,2,3,3,2,1]);
var arr = [...s];//[1,2,3]
//2、并集
var arr1 = [1, 2, 3];
var arr2 = [2, 3, 4];
var s = new Set([...arr1, ...arr2]); // {1, 2, 3, 4}
var arr = [...s];// [1,2,3,4]
//3、交集,arr1和arr2共有的元素
var arr1 = [1, 2, 3];
var arr2 = [2, 3, 4];
var s1 = new Set(arr1);//把数组转为set对象方便操作
var s2 = new Set(arr2);
var result = arr1.filter(x => s2.has(x));//[2, 3]
//4、差集,arr1去除arr2中的元素
var arr1 = [1, 2, 3];
var arr2 = [2, 3, 4];
var s1 = new Set(arr1);//把数组转为set对象方便操作
var s2 = new Set(arr2);
var result = arr1.filter(x => !s2.has(x));//[1]

Set值个数可以通过size属性获取

var s = new Set([1,2,3]);
console.log(s);//Set(3) {1, 2, 3}
console.log(s.size);//3

Set的方法

  1. **add():**添加新元素
  2. **delete():**删除指定元素
  3. **clear():**清空所有元素
  4. **has():**判断是否存在某值
  5. **forEach():**遍历每个元素,传入回调函数
  6. **keys():**返回一个 Iterator 对象,这个对象以插入Set 对象的顺序包含了原 Set 对象里的每个元素
  7. **values():**同keys()
  8. **entries():**返回 [value, value] 形式的数组迭代器对象,value 是给定集合中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序

add()、delete()、clear()、has() :

var s = new Set([1,2,3]);
s.add(4);
console.log(s);//Set(4) {1, 2, 3, 4}
s.delete(2);
s.has(2);//false
s.clear();
console.log(s.size);// 0

**keys()、values() :**返回一个 Iterator 对象,这个对象以插入Set 对象的顺序包含了原 Set 对象里的每个元素

var s = new Set([1,2,3]);
var setIter = s.values();
console.log(setIter);//SetIterator {1, 2, 3}
console.log(setIter.next().value); // 1
console.log(setIter.next().value); // 2
console.log(setIter.next().value); // 3

**entries() :**返回 [value, value] 形式的数组迭代器对象,value 是给定集合中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序

var s = new Set([1,2,3]);
var setIter = s.entries();
console.log(setIter);//SetIterator {1 => 1, 2 => 2, 3 => 3}
console.log(setIter.next().value); // [1, 1]
console.log(setIter.next().value); // [2, 2]
console.log(setIter.next().value); // [3, 3]

**forEach() :**遍历每个元素,传入回调函数,参数:回调函数、thisArg执行回调函数时可以当作this来使用。 回调函数参数:值(key)、值(value)、set对象

var s = new Set([1,2,3]);
s.forEach((key,value,set)=>{
console.log(key,value);//1 1, 2 2, 3 3
});

浅拷贝和深拷贝

**浅拷贝:**只拷贝最外面一层的数据;更深层次的对象,只拷贝引用,浅拷贝的时候,是属于传址,而非传值。 **深拷贝:**拷贝多层数据;每一层级别的数据都会拷贝,深拷贝会把对象里所有的数据重新复制到新的内存空间,是最彻底的拷贝。

**区分深拷贝与浅拷贝:**B复制了A,修改A,B也一样被修改是浅拷贝,B没变,是深拷贝。

var a = [0,1,2,3];
var b = a;// 浅拷贝,传地址
console.log(a===b);// 指向地址相同,true
a[0]=1;// 修改a
console.log(b[0]);// 1,b也改变

通过Object.assign()实现浅拷贝:

var obj1 = {
name: 'chuckle',
age: 19
};
var obj2 = Object.assign(obj1);
console.log(obj2);//{name: 'chuckle', age: 19}
obj2.name = "qx";
console.log(obj1.name);//qx,修改obj2,obj1也会被修改

实现深拷贝

通过for-in递归实现深拷贝,即递归遍历整个对象,找到简单值,将值复制

var obj1 = {
name: 'chuckle',
age: 19,
test: [1, 2, 3],
obj: {
name: 'giggles',
age: 18
}
};
var obj2 = {};
deepCopy(obj2,obj1);
console.log(obj2);//{name: 'chuckle', age: 19, test: Array(3), obj: {…}}
//修改obj2,不影响原来的obj1
obj2.name = 'qx';
console.log(obj1.name);//'chuckle'
console.log(obj2.name);//'qx'
function deepCopy(newObj, oldObj) {
//遍历属性名或数组下标
for (let key in oldObj) {
// 获取属性值 oldObj[key]
let item = oldObj[key];//依次获取属性值
//下面进行创建属性与属性值的复制
if (item instanceof Array) {
// 判断这个值是否是数组
newObj[key] = [];
deepCopy(newObj[key], item);//递归
} else if (item instanceof Object) {
// 判断这个值是否是对象
newObj[key] = {};
deepCopy(newObj[key], item);//递归
} else {
// 简单数据类型,直接赋值
newObj[key] = item;
}
}
}

通过JSON对象的parse和stringify方法实现深拷贝

function deepClone(obj){
let str = JSON.stringify(obj);//将对象转为字符串这个基本类型
return JSON.parse(str);//再将字符串转为新对象,新对象与原对象就没有关系了
}
var obj1 = {
name: 'chuckle',
age: 19,
test: [1, 2, 3],
obj: {
name: 'giggles',
age: 18
}
};
var obj2 = deepClone(obj1);
console.log(obj2);//{name: 'chuckle', age: 19, test: Array(3), obj: {…}}
//修改obj2,不影响原来的obj1
obj2.name = 'qx';
console.log(obj1.name);//'chuckle'
console.log(obj2.name);//'qx'

数组等对象的slice()不是完全的深拷贝,因为数组中存的是对象的引用地址,slice()只将地址拷贝了过去

var a = [0, 1, [1, 1, 1], 2, 3];
var b = a.slice();
//修改b的第一层不影响a
console.log(a[0]);// 0
b[0] = 9;
console.log(a[0]);// 0
console.log(b[0]);// 9
//修改嵌套的多层数组有影响
console.log(a[2][0]);// 1
b[2][0] = 0;
console.log(a[2][0]);// 0
console.log(b[2][0]);// 0

迭代器Iterator

迭代以从一个数据集中按照一定的顺序,不断取出数据的过程。

迭代与遍历的区别:

  1. 迭代强调依次取数据的过程,不保证把所有的数据都取完
  2. 遍历强调的是要把所有的数据依次全部取出

迭代器是能调用next()实现迭代的一种对象,该方法返回一个具有两个属性的对象(value:可迭代对象迭代至此的值,done:布尔,是否已经取出所有数据)

通过可迭代对象中的迭代器工厂函数Symbol.iterator来生成迭代器。每次生存的迭代器之间互不干扰。

var arr = [1, 2, 3, 4];
var arrIter = arr[Symbol.iterator]();
console.log(arrIter);// Array Iterator {}
while(true){
var next = arrIter.next();
var value = next.value;
var done = next.done;
if(done){
break;
}else{
console.log(value);// 1 2 3 4
}
}

迭代器对象可作为可迭代对象,for-of遍历可迭代对象

var arr = [1, 2, 3, 4];
var arrIter = arr[Symbol.iterator]();
for (let i of arrIter) {
console.log(i);// 1 2 3 4
}

如果可迭代对象在迭代期间被修改了,迭代器得到的结果也是修改后的。

var arr = [1, 2, 3, 4];
var arrIter = arr[Symbol.iterator]();
for (let i of arrIter) {
arr[2] = 0;
console.log(i);// 1 2 0 4
}

当迭代到 done: true 时迭代器会处于一种完成但并不完成的状态,还能重复调用 next(),结果都是 { value: undefined, done: true }

正则表达式

**正则表达式:**用某种模式去匹配一类字符串的公式,正则表达式在线测试

正则表达式主体和修饰符:

i:不区分大小写的匹配
g:全局匹配(匹配所有匹配而非在找到第一个匹配后停止)
m:多行匹配
[abc] 匹配方括号之间的任何字符
[^abc] 任何不在方括号之间的字符
[0-9] 匹配 0 到 9 的数字。
[a-z] 匹配从小写 a 到小写 z 的字符
(aa|bb|cc) 匹配aa或bb或cc
.(点号) 匹配单个字符,除了换行和行结束符
\w 匹配数字、字母及下划线。
\W 匹配非单词字符
\d 匹配数字
\D 匹配非数字字符
\s 匹配空白字符
\S 匹配非空白字符
\b 匹配单词边界
\B 匹配非单词边界
\n 匹配换行符
\r 匹配回车符
\t 匹配制表符
\f 匹配换页符
\v 匹配垂直制表符
\0 匹配null字符
\\ 匹配\
\" 匹配 "
\' 匹配 '
\xxx 匹配以八进制数 xxx 规定的字符
\xdd 匹配以十六进制数 dd 规定的字符
\uxxxx 匹配以十六进制数 xxxx 规定的 Unicode 字符
+ 重复1次或更多次
* 重复任意次数
{n} 重复n次
{n,} 重复n次或更多次(最少n次)
{n,m} 重复n到m次
^ 限定开始位置的字符
$ 限定结尾位置的字符,如果在正则表达式中同时使用^和$符号,则要求字符串必须完全符合正则表达式
? 非贪婪模式,找到到第一个就不再往后匹配,正则默认贪婪匹配,在同一个匹配项中,尽量匹配更多所搜索的字符
?=n 匹配其后有紧接指定字符串 n 的字符串
?!n 匹配其后没有紧接指定字符串 n 的字符串
?<=n 找到n但不匹配n

RegExp对象

在js中,正则表达式也是对象,RegExp是一个预定义了属性和方法的正则表达式对象

var re = /正则表达式主体/修饰符(可选);// re = /Hello/g
var re = new RegExp("正则表达式"); // 参数是字符串
var re = new RegExp("正则表达式", "匹配模式"); // 两个参数都是字符串

RegExp对象的属性和方法:

  1. global: 判断是否设置了 "g" 修饰符
  2. **ignoreCase:**判断是否设置了 "i" 修饰符
  3. **multiline:**判断是否设置了 "m" 修饰符
  4. **lastIndex:**规定下次匹配的起始位置
  5. **source:**返回正则表达式的匹配模式
  6. **test():**判断指定字符串是否符合正则规则,返回布尔
  7. **exec():**返回一个数组,存放正则匹配的结果。无匹配返回 null
  8. **toString():**返回正则表达式的字符串。

exec()在 regexp 的属性 lastIndex 指定的字符处开始检索字符串,当它找到了与表达式相匹配的文本时,在匹配之后,它将把 regexp 的 lastIndex 属性设置为匹配文本后的第一个字符所在位置(调用test()也会改变lastIndex),可以通过反复地调用 exec() 方法来遍历字符串中的所有匹配文本,当 exec() 再也找不到匹配的文本时,它将返回 null,并且把属性 lastIndex 重置为 0

**返回值:**匹配到的文本的数组,数组有四个属性,index 匹配文本第一个字符的位置,input 需匹配的原字符串,groups 当初中命名的分组时匹配到的分组对象

var str="Hello world! Hello china!";
var re = /hello/gi;
console.log(re.lastIndex);// 0
console.log(re.test(str));// true
console.log(re.lastIndex);// 5
console.log(re.exec(str));// true
//['Hello', index: 13, input: 'Hello world! Hello china!', groups: undefined]
console.log(re.lastIndex);// 18

检查一个字符串是否是一个合法手机号

以1开头(^1 表示1开头) 第二位是3~9之间任意数字[3-9] 三位以后任意9位数字[0-9]{9}重复9次的0-9

var str = "15123456789";
var re = /^1[3-9][0-9]{9}$/;
console.log(re.test(str));// true

判断字符串是否为电子邮件

var str = "916017604@qq.com"
var re = /^\w{3,}(\.\w+)*@[A-z0-9]+(\.[A-z]{2,5}){1,2}$/
console.log(re.test(str));// true

支持正则表达式的 String 对象的方法:

  1. search() 使用表达式来搜索匹配,然后返回匹配的位置
  2. replace() 返回模式被替换处修改后的字符串,不改变原字符串,返回替换后的字符串
  3. match() 返回匹配到的字符串的数组
var str = "你好世界";
var n = str.match(/你好/);
console.log(n[0]);// 你好
console.log(str.replace(/世界/, '太阳系'));// 你好太阳系
var str = "我的电话号码是15123456789";
var n = str.match(/1[3-9][0-9]{9}/);
console.log(n[0]);// 15123456789
var str = "http://127.0.0.1:4000/";
var re = /(?<=(http|https):\/\/).+?(?=\/)/g;
var n = str.replace(re, 'www.qcqx.cn');
console.log(n);// http://www.qcqx.cn/

原型与原型链

省流: 原型其实就是一个对象,实例继承原型对象的属性,通过继承的这种方式,new出来的实例也有了这个属性

对象的构造函数有一个 prototype 的属性,通过这个属性就能访问到原型

对象有一个 __proto__ 属性,指向构造函数的 prototype 的属性,所以也可以访问到原型

构造函数也有 __proto__ 属性,也是对象,因为所有函数都是 Function 构造的,所以都等于 Function.prototype

原型也是对象,也有 __proto__ 属性,通常浏览器称之为 [[Prototype]] ,它指向原型的原型,像单链表的next指针一样,构成原型链,直到Object构造函数,因为 Object 对象的原型 Object.prototype.proto 为空null没有下一个原型了,Object.prototype是原型的终点

原型与原型链其实是为了实现继承,原型就像java中的类,由构造函数构造的对象都继承了这个构造函数原型的属性和方法,当然,每个对象后续都可以添加自己的属性和方法来覆盖继承自原型的属性和方法。

原型链中每个节点是构造函数的原型,每个节点有一个循环:构造函数的 prototype 属性指向它的原型,原型的 constructor 属性指向它的构造函数

普通对象也可以利用 Object.create() 作为新生成对象的原型。

原型

每个 JS 对象一定对应一个原型对象,并从这个原型对象继承属性和方法。

对象只有隐式原型 __proto__ 函数有隐式原型 __proto__ ,还有显式原型 prototype

对象的 __proto__ 指向 其构造函数.prototype,或者说 对象.__proto__ 等于 其构造函数.prototype 保存的东西是一样的 函数的 __proto__ 都指向Function.prototype,因为所有函数都是由Function构造的,值为 ƒ(){[native code]}

函数的 prototype 指向一个对象,就是通常所说的原型对象,一定有两个属性,constructor 指向构造函数本身,[[Prototype]]就是__proto__,它连接起一个个原型,构成原型链,Object 对象的原型 Object.prototype.proto 为空null

两种获取对象隐式原型的方式:对象的__proto__ 属性(非标准),Object.getPrototypeOf()方法(标准) **获取显式原型:**构造函数.prototype

var obj = new Object();
console.log(obj.__proto__);
console.log(Object.getPrototypeOf(obj));
//{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
// constructor: ƒ Object()
// __proto__: null 原型的__proto__指向下一个原型,而Object对象的原型 Object.prototype.__proto__ 为空null
// 原型上还有各种方法,所有Object和Object的子类都会继承这些属性和方法
console.log(Object.__proto__);
console.log(Function.prototype);
//ƒ () { [native code] }

只有一个Object对象不够直观,新建一个构造函数,new一个Person对象

function Person() {
this.a = 1;
}
var person = new Person();
console.log(person.__proto__);
console.log(Object.getPrototypeOf(person));

下面的输出就是Person对象的原型,可以发现原型上并没有a属性,在构造函数中通过this直接定义实例成员,会作为实例对象的属性,而不是出现在原型上再被对象继承。

{constructor: ƒ}
constructor: ƒ Person()
[[Prototype]]: Object

这个原型上只有两个属性,我们可以在原型上添加属性和方法,所有对象都会继承这些属性和方法,哪怕是在这之前已经实例化的对象。

function Person() {
this.a = 1;
}
var person = new Person();
//在原型上添加属性和方法
Object.getPrototypeOf(person).a = 0;
Object.getPrototypeOf(person).b = 2;
console.log(Object.getPrototypeOf(person));

可以看到原型上出现了a属性,值为0

{a: 0, constructor: ƒ}
a: 0
b: 2
constructor: ƒ Person()
[[Prototype]]: Object

试着在实例化对象中使用继承来的a、b属性

function Person() {
this.a = 1;
}
var person = new Person();
//在原型上添加属性和方法
Object.getPrototypeOf(person).a = 0;
Object.getPrototypeOf(person).b = 2;
//使用继承来的a、b属性
console.log(person.a);// 1
console.log(person.__proto__.a);// 0
console.log(person.b);// 2

person.a 输出 1,访问的是实例成员a,也就是说对象的属性可以覆盖继承来的同名属性

1
0
2

再看原型中的的 constructor 属性,它指向该原型对应的构造函数,可以在控制台中展开

构成了一个循环:构造函数的 prototype 属性指向它的原型,原型的 constructor 属性指向它的构造函数

原型链

原型是原型链上的节点,各个原型通过 __proto__ 相连接,对象可以继承原型链上从Object构造函数开始至该对象构造函数的所有原型的属性和方法,即继承了其所有父级的所有属性和方法,且逐层可以覆盖

制造原型链:

直接操作prototype属性:

function Parent(){}
function Child(){
this.a=1;
}
Parent.prototype.b = 2;// 在原型上添加属性
Parent.c = 0;//在构造函数上添加静态成员
// 通过原型的__proto__连接起两个原型,表示Child继承Parent
Child.prototype.__proto__ = Parent.prototype
// 也可以通过setPrototypeOf设置原型的原型,来连接两个原型
Object.setPrototypeOf(Child.prototype,Parent.prototype);
var child = new Child();
console.log(child.a);// 1,访问实例成员
console.log(child.b);// 2,通过原型链继承自Parent原型上的b属性
console.log(child.c);// undefined,不能通过原型链访问原型对应构造函数的静态成员

可以在控制台展开 [[Prototype]] 属性来查看原型链

通过 Object.create() 建立原型链,在Vue源码中 Object.create() 的使用频率非常高

Object.create() 用于创建一个新对象,使用现有的对象来作为新创建对象的原型

原型和普通对象都有__proto__和constructor属性,且作用都一样,利用Object.create()可以将一个普通对象作为新对象的原型

function Parent(){
this.p = 0;
}
Parent.prototype.b = 2;
var parent = new Parent();
parent.a = 1;
// 使用现有的对象来作为新创建对象的原型
var child = Object.create(parent);
console.log(child.b);// 2,通过原型链继承自Parent原型上的b属性
console.log(child.p);// 0
console.log(child.a);// 1

将原型与构造函数的循环、原型与原型的链、构造函数的实例组成一张图:

评论

快速前往