JS高级
一.Object.prototype.toString.call(xx)
这个可以用来准确判断xx类型,原理就是调用原型链上Object.prototype的toString方法,但是将参数改成了xx。
有一道面试题:使用typeof bar === "object"
检测”bar”是否为对象有什么缺点?如何避免?
这是一个十分常见的问题,用 typeof
是否能准确判断一个对象变量,答案是否定的,null
的结果也是 object,数组的结果也是 object,有时候我们需要的是 "纯粹" 的 object 对象。如何避免呢?比较好的方式是:
console.log(Object.prototype.toString.call(obj) === "[object Object]");
使用以上方式可以很好的区分各种类型:
(无法区分自定义对象类型,自定义类型可以采用instanceof区分)
console.log(Object.prototype.toString.call("jerry"));//[object String] console.log(Object.prototype.toString.call(12));//[object Number] console.log(Object.prototype.toString.call(true));//[object Boolean] console.log(Object.prototype.toString.call(undefined));//[object Undefined] console.log(Object.prototype.toString.call(null));//[object Null] console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object] console.log(Object.prototype.toString.call(function(){}));//[object Function] console.log(Object.prototype.toString.call([]));//[object Array] console.log(Object.prototype.toString.call(new Date));//[object Date] console.log(Object.prototype.toString.call(/\d/));//[object RegExp] function Person(){}; console.log(Object.prototype.toString.call(new Person));//[object Object]
为什么这样就能区分呢?于是我去看了一下toString方法的用法:toString方法返回反映这个对象的字符串。
那为什么不直接用obj.toString()呢?
console.log("jerry".toString());//jerry console.log((1).toString());//1 console.log([1,2].toString());//1,2 console.log(new Date().toString());//Wed Dec 21 2016 20:35:48 GMT+0800 (中国标准时间) console.log(function(){}.toString());//function (){} console.log(null.toString());//error console.log(undefined.toString());//error
const a = {a:1};a.toString(); //[object Object]
同样是检测对象obj调用toString方法(关于toString()方法的用法的可以参考toString的详解),obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?
这是因为toString为Object的原型方法,而Array 、Function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(Function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串.....),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;obj比较特殊,它没有重写toString方法。因此,在想要得到对象的具体类型时,应该调用Object上原型toString方法。
我们可以验证一下,将数组的toString方法删除,看看会是什么结果:
var arr=[1,2,3]; console.log(Array.prototype.hasOwnProperty("toString"));//true console.log(arr.toString());//1,2,3 delete Array.prototype.toString;//delete操作符可以删除实例属性 console.log(Array.prototype.hasOwnProperty("toString"));//false console.log(arr.toString());//"[object Array]"
删除了Array的toString方法后,同样再采用arr.toString()方法调用时,不再有屏蔽Object原型方法的实例方法,因此沿着原型链,arr最后调用了Object的toString方法,返回了和Object.prototype.toString.call(arr)相同的结果。
二、原型链
prototype、__proto__和[[Prototype]]的区别
prototype大家比较了解,这里就不说了。今天说一下__proto__和[[Prototype]].执行以下代码时,浏览器控制台里发现了b有[[Prototype]]这个属性,并且顺着[[Prototype]]链一直点下去,发现这个链和常规理解的原型链是不一样的。
function AA() { this.a = 1; } const b = new AA(); console.log(b);
搜索了一下,发现[[Prototype]]是js一个内部属性,无法访问和修改,所以在ES6才有了__proto__。__proto__是用来作为[[Prototype]]的代理的,通过它可以读取和修改[[Prototype]]。
注意不要把__proto__
当做标准来使用,更多的时候我们应该使用Object.setPrototypeOf()
(写操作)、Object.getPrototypeOf()
(读操作)、Object.create()
(生成操作)代替。
function Person(name) { this.name = name; } var p = new Person("zhenglijing"); //等同于将构造函数的原型对象赋给实例对象p的属性__proto__ p.__proto__ = Object.setPrototypeOf({},Person.prototype); Person.call(p,"zhenglijing");
至于在控制台中的打印,我们可以直接忽略[[Prototype]],只看__proto__,就可以看到正确的原型链了。
三、js中的类(class)
es5中定义一个类
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; var p = new Point(1, 2);
es6中定义一个类是用class,它是对传统方法的一个语法糖,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。也可以把它看作构造函数的另一种写法。
一般来说我们都是用构造函数来声明实例属性,用原型对象来声明方法。但是如果想在原型对象上定义属性的话,在class中需要写成value() return {123}这种
//Person.js class Person{ // 构造函数 constructor(x,y){
//constructor内部的this,指向的是实例本身 this.x = x; this.y = y; }; status = 1; // 在实例的原型对象上 fun = () => {console.log(11)}; // 在实例的原型对象上
toString() {
return (this.x + "的年龄是" +this.y+"岁"); } } let person = new Person('张三',12); console.log(person.toString());
以下代码表明,类的数据类型就是函数,类本身就指向构造函数。使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
class Point { // ... } typeof Point // "function" Point === Point.prototype.constructor // true
注意:①类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法
class Point { constructor(){ // ... } } Object.assign(Point.prototype, { toString(){}, toValue(){} });
prototype对象的constructor属性,直接指向“类”的本身,这与 ES5 的行为是一致的。
②constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
class Foo { constructor() { return Object.create(null); } } new Foo() instanceof Foo // false
上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
③class内的constructor方法内部定义的this,是定义在类的实例上,而class内的constructor方法外定义的方法,是定义在类的原型对象,即类.prototype上。这个看一下class的语法糖就明白了。
四、js中类的constructor、super
首先,ES6 的 class
属于一种“语法糖”,所以只是写法更加优雅,更加像面对对象的编程,其思想和 ES5 是一致的。
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function() { return '(' + this.x + ',' + this.y + ')'; }
等同于
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ',' + this.y + ')'; } }
其中 constructor 方法是类的构造函数,是一个默认方法,用来初始化对象方法,通过 new 命令创建对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个默认的 consructor 方法会被默认添加。所以即使你没有添加构造函数,也是会有一个默认的构造函数的。一般 constructor 方法返回实例对象 this ,但是也可以指定 constructor 方法返回一个全新的对象,让返回的实例对象不是该类的实例。
我们还可以向类的方法发送参数:
class Runoob { constructor(name, year) { this.name = name; this.year = year; } age(x) { return x - this.year; } } let date = new Date(); let year = date.getFullYear(); let runoob = new Runoob("菜鸟教程", 2020); "菜鸟教程 " + runoob.age(year) + " 岁了。";
而在 ES6 中,super 是一个特殊的语法,而且它比 this 还要特殊,有很多用法上的限制。
super类似于ES5语法中的call继承
class A{ constructor(n){ console.log(n); //=>100; this.x = 100; } getX(){ console.log(this.x); } } class B extends A{//=>extends 类似实现原型继承 constructor(){ super(100);//=>类似于call的继承:在这里super相当于把A的constructor给执行了,并且让方法中的this是B的实例,super当中传递的实参都是在给A的constructor传递。 this.y = 200; } getY(){ console.log(this.y); } } let f = new B(); // 100
let g = nwe A(); // undefined
对于使用来说,super 关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
1、super当做函数使用
super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super() 函数。注意:作为函数时,super() 只能用在子类的构造函数之中,用在其他地方就会报错。
class A {} class B extends A { constructor() { super(); } }
super 作为函数调用时,内部的 this 指的是子类实例
class A { constructor() { this.show(); } } class B extends A { constructor() { super(); } show(){ console.log('实例'); } static show(){ console.log('子类'); } } new B() //输出 '实例' ,new B 时触发了 B 的构造函数,所以触发了 super 方法,即触发了父类 A 的构造函数,此时的 this.show 的 this 指的是子类
new A() // 报错,this.show is not a function
2、super 作为对象使用
super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法(static)中,指向父类;
方法内部的this,在普通方法中,指向当前的子类实例;在静态方法(static)中,指向当前的子类。
2.1、super在普通方法中(即非静态方法)及此时的 this 关键字指向
class A { p() { return 2; } } class B extends A { constructor() { super(); console.log(super.p()); // 2 此时的super指向父类原型对象,即 A.prototype } } let b = new B(); //2
由于在普通方法中的 super 指向父类的原型对象,所以如果父类上的方法或属性是定义在实例上的,就无法通过 super 调用的。如下所示:
class A { constructor() { //在构造函数上定义的属性和方法相当于定义在父类实例上的,而不是原型对象上 this.p = 2; } } class B extends A { get m() { return super.p; } } let b = new B(); console.log(b.m) // undefined
在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向的是当前的子类实例。
class A { constructor() { this.x = 1; } print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; super.y = 123; //如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。 } m() { super.print(); } } let b = new B(); b.m() // 2 console.log(b.y); //123
2.2、super在静态方法中及此时的 this 关键字指向
super作为对象,用在静态方法之中,这时 super 将直接指向父类,而不是父类的原型对象。
class Parent { static myMethod(msg) { console.log('static', msg); } myMethod(msg) { console.log('instance', msg); } } class Child extends Parent { static myMethod(msg) { super.myMethod(msg); //指向class Parent } myMethod(msg) { super.myMethod(msg); //指向class Parent.prototype } } Child.myMethod(1); // static 1 var child = new Child(); child.myMethod(2); // instance 2
在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例。
class A { constructor() { this.x = 1; } static print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } static m() { super.print(); } } B.x = 3; //这里比较绕,意思就是static print()中的this指向的是B B.m() // 3
五、js中类的静态方法和setter、getter
静态方法
是使用 static 关键字修饰的方法,又叫类方法,属于类的,但不属于对象,在实例化对象之前可以通过 类名.方法名 调用静态方法。
静态方法不能在对象上调用,只能在类中调用。
class Runoob { constructor(name) { this.name = name; } static hello() { return "Hello!!"; } } let noob = new Runoob("菜鸟教程"); // 可以在类中调用 'hello()' 方法 Runoob.hello(); // 'Hello!!' // 不能通过实例化后的对象调用静态方法 noob.hello(); //报错
如果你想在对象 noob 中使用静态方法,可以作为一个参数传递给它:
class Runoob { constructor(name) { this.name = name; } static hello(x) { return "Hello " + x.name; } } let noob = new Runoob("菜鸟教程"); Runoob.hello(noob);
getter 和 setter (vue就是用这两个实现数据驱动视图的)
类中我们可以使用 getter 和 setter 来获取和设置值,getter 和 setter 都需要在严格模式下执行。getter 和 setter 可以使得我们对属性的操作变的很灵活。
①get和set不会检测到constructor中的数据变化 ② set和get 是设置在类的原型上的,在以下例子中就是Runoob.prototype上
类中添加 getter 和 setter 使用的是 get 和 set 关键字,以下实例为 sitename 属性创建 getter 和 setter:
class Runoob { constructor(name) { this.sitename = name; } get s_name() { return this.sitename; } set s_name(x) { this.sitename = x; } } let noob = new Runoob("菜鸟教程");
noob.s_name = "RUNOOB"; noob.s_name;
注意:即使 getter和setter 是一个方法,当你想获取和修改属性值时也不要使用括号。
getter/setter 方法的名称不能与属性的名称相同,在本例中属名为 sitename。
很多开发者在属性名称前使用下划线字符 _ 将 getter/setter 与实际属性分开:
以下实例使用下划线 _ 来设置属性,并创建对应的 getter/setter 方法:
class Runoob { constructor(name) { this._sitename = name; } get sitename() { return this._sitename; } set sitename(x) { this._sitename = x; } } let noob = new Runoob("菜鸟教程");
noob.sitename = "RUNOOB"; noob.sitename;
拓展:看到这里好像有点疑惑,怎么跟Object.defineProperty里设置的get和set不太一样。比如上面的例子,在类里面,想触发set或get都只能触发set和get的函数名,而不是直接触发类的名称。但其实仔细想想就明白了,一个类有那么多属性,如果只有一个get和set,怎么一一绑定呢。继续深入了解,发现sitename和s_name完全是两个东西,他俩的关系就像是vue里面data和computed的关系 (其实应该是vue模仿set和get,但谁让它先入为主呢),所以我获取s_name就获取了sitename的值,修改s_name就修改了sitename的值。
class Runoob { constructor(name) { this.sitename = name; } get s_name() { return this.sitename; } set s_name(x) { this.sitename = x; } } let b = new Runoob(); Object.defineProperty(b,'sitename',{ //这里相当于用给sitename设置了同名的get和set函数,同名的get和set只有Object.defineProperty可以设置 get(){return 666}, set() {console.log(123)} }); b.sitename = 4; // 打印123 b.sitename; //666
打印了vue实例data中的get和set,确实就是基于对象属性创建的同名的get和set,也就说明vue的get和set是用Object.defineProperty创建的。
六、构造函数
区别是不是构造函数,就看这个已经被声明的函数,是不是被new出来的。
this和prototype的区别
// 创建函数 function Person(name, age) { this.name = name this.age = age this.testArr = [] // 使用this定义属性或方法 this.getAge = function () { console.log(this.age) } } // Person.prototype(原型对象,实质也是对象) // 使用prototype定义属性或方法 Person.prototype.getName = function() { console.log(this.name) } Person.prototype.arr = ['0'] // 通过new操作符来调用的,就是构造函数,如果没有通过new操作符来调用的,就是普通函数 const preson = new Person('jian', 56) const preson1 = new Person('jian', 56) console.log('使用prototype和this定义方法能正常使用') preson.getName() //'jian' preson.getAge() //56 preson1.getName() //'jian' preson1.getAge() // 56 console.log('使用prototype定义属性') preson.arr.push('1') console.log('preson.arr', preson.arr) // ['0','1'] console.log('preson1.arr', preson1.arr) // ['0','1']
console.log('使用this定义属性')
preson.testArr.push('1')
console.log('preson.testArr', preson.testArr) //['1']
console.log('preson1.testArr', preson1.testArr) // []
// 属性constructor(指针) ,又指向 Person函数对象
console.log(Person.prototype.constructor)
this定义的方式,实例化之后是让每一个实例化对象都有一份属于自己的在构造函数中的对象或者函数方法;
而prototype定义的方式,实例化之后每个实例化对象共同拥有一份构造函数中的对象或者函数方法。
所以用this来定义构造函数的属性较多,用prototype定义构造函数的方法较多
new关键字做了什么
- 创建类的实例:创建一个空对象obj,然后把这个空对象的__proto__设置为Person.prototype(即构造函数的prototype);
- 初始化实例对象:构造函数Person被传入参数并调用(使用apply方法触发构造函数的constructor),让构造函数的this指向该实例obj;
- 返回实例obj;
New实现过程(类似):
function New(F) { const obj = { '__proto__': F.prototype } return function () { F.apply(obj, arguments) return obj } } //写个demo测试一下 function Person(){ this.x = 1; this.y = 2; } let p = New(Person)(); console.log(p)