javascript是一门垃圾语言
javascript设计之初是没有考虑很多的,别管这门语言的作者什么背景,在怎么牛逼的人也不能在一周之内把语言设计的很好的。
就拿最基本的数据类型来说,javascript对引用类似唯一提供的就是对象,且实现有问题(详见对象的遍历顺序)。
我很喜欢java,它是一门标准的面对对象语言,一切皆对象,即使有int,float等关键字的遗留,但那是为了照顾老人,还是可以通过封装类来达到对象的转化。
javascript原型链是个啥
var obj={
a:1
};
var obj1={
b:2
}
Object.setPrototypeOf(obj1,obj);
这样,obj就在obj1的原型链上了。 然后obj1.__proto__===obj 了。这就是原型链。 obj在obj1的链上,obj1可以访问obj的属性,这就叫做原型属性。
javascript的继承
比较奇怪的是js面向对象的方式。 在java里,一个对象要创建出来需要new,new是一个很重量级的操作,需要JVM进行一系列操作,它不像简单的堆栈运算,毕竟涉及到对象。 而非常困惑我当初作为javaer的就是js里的对象。 我明白JSON和js对象的爱恨情仇,详见json RFC草案标准,所以我认为js不会把一个使用这么频繁的东西搞得很复杂,开销很大。果真,js是没有对对象做很复杂的操作的。 那,作为一个承载数据的数据结构,js如何实现OOP范式呢? 这就不得不从js的版本号说起。 现在的js全部都是从ecmascript开始流行起来的, es2015是没有class关键字的,因此无法引入面向对象,但是自从es2015,js就已经很火很火了,怎么办?难道一大堆英雄好汉?不,js还有一个保留字—new。于是,可能在ES39某次会议上,大家决定可以使用函数来作为对象使用。于是,函数这种数据结构,你就不得不重视起来,因为它等价于class。底层是用它封装的语法糖形成的class关键字,但是这个底层是开发浏览器的底层,作为一名业务层程序员还是欣赏不到的。所以,怎么写?
function Student(_name,_age){
this.name=_name;
this.age=_age;
}
Student.prototype.name;
Student.prototype.age;
Student.prototype.run(){
console.log(this.name+":run");
}
Student.play(){
console.log("学生都爱玩");
}
Student();
let stu = new Student();
简单来说,es5为了可以面向对象,因此使用了new保留字。函数也是一个对象啊,函数里面的内容如何区分它是成员属性还是成员方法?就引入了一个prototype指针 。这个指针指向的区域是这个函数实例化后形成的成员属性和方法指向的区域。也因此这个实例就可以拥有这些属性和方法。静态属性就直接放到类名. 引用,这个很简单,没啥好说的。而new Student()才可以让这个函数真正成为一个面向对象,形成一个对象。形成的这个对象stu 里面的属性放到了原型链上,我们之前说过,所有原型链上的属性和方法叫做原型属性和方法,本身不属于这个对象。也因此stu.__proto__ 和Student.prototype 有一块儿缓冲的地方做交流和沟通。stu 是普通的JSON可以沟通的对象,而它和普通对象有点不同的是直接创建的对象的原型链上直接就指向Object的原型了。这点有点类似于java里面的直接以Object new对象。js里面创建对象就相当于一个语法糖吧。稍等,让我们说明白java 里面的对象为什么和js 里面的对象不是一个重量级的。每次java new对象,都会在线程变量里面保存一个独一的类的副本,往这个父本上填写对象。假如java new的这个对象是一个数据结构。java需要这个数据结构的很多实例,那么每个对象都能拿到独一无二的数据结构API方法的一套父本,而我们创建对象更多的考虑的是怎么用里面的数据,毕竟pojo 为代表的java对象是大量使用的嘛。也因此,js做出了一个完全不同的改变。将对象保存、承载数据的理念发挥到极致,但是为了引入对于实例属性和方法的描述,给出了一个原型指针作为属性。(这个原型指针 就是也是一个对象,是一个相当于这个类的静态方法和属性动态使用的一种策略)。这样,每次new对象,相当于简单的为这个对象填入了对象属性,原型属性则通过一个指针来进行链接,拿到这个原型下所有API。这样多好,就拿到了这个数据结构的所有内容,而且还是保存在一份内存中。对象又可以简单的只是承载数据,就太灵活了。。。 而对于构造器的使用,因为构造器很像这个函数本身,这个函数一大些,就相当于这个构造器了,所以Student.prototype.constructor===Student 。 这样写不太好理解,那就列一个等式: stu.__proto__===Student.prototype stu.__proto__.constructor===这个类的构造器===只执行一次的函数,按照es5的构造,这个代码片段可以放在那里就很清晰了,就是new Student时的那个函数里面的内容。所以 stu.__proto__.constructor===Student 根据等式: stu.__proto__.constructor===Student===Student.prototype.constructor , 再联想到Java里构造器的名字和类名相同,Student就是一个构造器,其放了一个指针在对象里面,使得对象 可以直接通过 对象名.constructor的方式来访问到这个构造器。而这个构造器是这个类/函数对象,这个类/函数对象里面又有一个prototype属性,这个prototype属性又会有constructor属性,…如此就形成了一个循环,其实就是在浏览器手动点击一下就相当于顺着这个指针找内容,不点肯定不是递归的,因此也不会产生很大的存储浪费。所以对象深复制时,当拿到对象的这个属性的key为constructor时,就代表了遍历了这个对象,此时因为拿到的是自己的对象属性,即使对象可以访问其原型属性,它也肯定是进入了其原型链这个对象(__proto__ )里面去进行拷贝,此时的source是这个原型链对象,如果遇到constructor对象,因为constructor对象下面是这个函数对象,函数对象下面又会有prototype属性,prototype属性里面又有constructor,赶快跳出来,于是就简单的将此时得到的这个键为constructor的这个对象的值–>一块引用地,的constructor(原型对象)或者直接将他自己对象属性给出去,然后完成克隆。 再说一下js里面的包装类型。 js也是有堆栈的,这样写和这样写完全不一样:
var n1=1;
var n2=new Number(1);
一个只是在栈中存储了一个值而已。 另一个只是在堆中存储了一个对象,并且把这个对象的地址赋值给了栈中的一个引用而已。 但是为什么只要将1赋值给一个引用地址,这个引用地址就可以找到Number 类对应的API呢?很简单,在解析n1的数据类型时(js是弱类型,需要运行时动态解析数据类型,这也是snippets不太友好的原因之一),可以将其映射到对应的Number类。这本身也是一种引用,这种引用就和指针直来直去的不太一样了,不过也是引用的一种。 所以,n1和n2也就完全不一样了。 而且null 和undefined 没有所谓的引用,是因为在Js中,并没有对应的包装类,因此他们就只能以第一种形态存储在栈中而变成一种值。 但是typeof null为什么是对象?很简单,也是用了这个技术,将内存直接映射到了Object上。因为Number最终也是要通过原型链找到Object的。只不过Number好像也是es5面向对象继承过来的,因此Number->Function->Object这样一个顺序。但是为什么undefined没有,而是undefiend呢?我认为这是一个10亿美元的bug造成的,就是引入了null 这个设计,好多程序引入了这个值造成了臭名昭著的空指针异常,但是我喜欢的Rust没有这个缺陷,很舒服。所以当初最初的时候可能就只有undefined这个判断类型,这个null也是在不得已情况下加上去的。
继承中的this指向除了执行父类构造器中的this是父类外,其他甚至于父类的方法都是指向的是子类对象。 也因此js里面的很多设计模式根本没办法做,es6太弱,没有接口,es5写面向对象还需要重构。就很恶心。所以用ts做。
增量理解
图: 继承的理解
es5实现面相对象
function Fu(_a) {
this.a = _a;
console.log("i am fu");
}
Object.defineProperties(Fu.prototype, {
hello: {
value() {
console.log('hello');
}
}
});
function Zi(_a) {
this.super(_a);
console.log('I am zi');
}
Object.defineProperties(Zi.prototype, {
sayHello: {
value() {
this.superFn("hello");
console.log('say hello');
}
}
});
function extendz(subClass, supClass) {
Object.setPrototypeOf(subClass.prototype, supClass.prototype);
Object.defineProperties(subClass.prototype, {
super: {
value: function () {
supClass.apply(this, arguments);
}
},
superFn: {
value: function (fnName) {
return supClass.prototype[fnName].apply(this, Array.from(arguments).slice(1));
}
}
});
}
extendz(Zi, Fu);
var zi = new Zi(2);
console.log(zi);
zi.hello();
修改原型写法:
function Fu(_a) {
this.a = _a;
console.log("i am fu");
}
Object.defineProperties(Fu.prototype, {
hello: {
value() {
console.log('hello');
}
}
});
function Zi(_a) {
this.super(_a);
console.log('I am zi');
}
Object.defineProperties(Zi.prototype, {
sayHello: {
value() {
this.superFn("hello");
console.log('say hello');
}
}
});
function extendz(subClass, supClass) {
Object.setPrototypeOf(subClass.prototype, supClass.prototype);
Object.defineProperties(subClass.prototype, {
super: {
value: function () {
supClass.apply(this, arguments);
}
},
superFn: {
value: function (fnName) {
return supClass.prototype[fnName].apply(this, Array.from(arguments).slice(1));
}
}
});
}
extendz(Zi, Fu);
var zi = new Zi(2);
console.log(zi);
zi.hello();
注:用js实现有诸多不便,super是一个关键字,是浏览器的内核实现的,所以这里只是模仿其写法,不必纠结完全一样,思想会了就可以了。
|