Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入理解 JavaScript prototype #4

Open
yijinc opened this issue May 9, 2019 · 0 comments
Open

深入理解 JavaScript prototype #4

yijinc opened this issue May 9, 2019 · 0 comments
Labels
blog blogs and articles

Comments

@yijinc
Copy link
Owner

yijinc commented May 9, 2019

var str = 'Hello world!';    // var str = new String('Hello world!')
str.substr(2, 4);
str.indexOf('world');

我们经常可以用到的字符串函数 substrreplaceindexOf等,是因为 String 对象上的 prototype 预先定义了这些方法。strString 的一个实例。

JavaScript标准库中常用的内置对象上 prototype 的方法还有:
Array​.prototype​.pushArray​.prototype​.push
Date​.prototype​.get​DateDate​.prototype​.get​Year
Function​.prototype​.toStringFunction​.prototype​.call
...

我们使用构造函数创建一个对象:

function User() {
}
var user = new User();
user.name = '张三';
console.log(user.name) // 张三

在这个例子中,User 是一个构造函数,我们使用 new 创建了一个实例对象 user

prototype


JavaScript 不包含传统的类继承模型,而是使用 prototype 原型模型。
每个函数都有一个 prototype 属性,换句话说, prototype 是函数才会有的属性

function User() {
}
User.prototype.name = '张三';
var user1 = new User();
var user2 = new User();
console.log(user1.name) // 张三
console.log(user2.name) // 张三

函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的 实例 的原型。也就是说这个例子中 User 的属性 prototype 对象是 user1 和 user2 的原型。

既然函数的 prototype 属性指向了一个对象,我们可以重写原型对象

function User() {}
User.prototype = {
    name: '张三',
    greeting: function() {
        console.log('hello!')
    }
};
var user = new User();
console.log(user.name);   // 张三
user.greeting();  // hello!

这样,我们就可以 new User 对象以后,就可以调用 greeting 方法了。

然而将原子类型赋给 prototype 的操作将会被忽略

function User() {}
User.prototype = 1 // 无效

我们还可以在赋值原型 prototype 的时候使用 function 立即执行的表达式来赋值:

function User() {}
User.prototype = function() {
    name = '张三',
	greeting = function() {
        console.log('hello!', this.name)
    }
    return {
	    name: name,
	    greeting: greeting
    }
}();
(new User()).greeting();

它的好处就是可以封装私有的 function,通过 return 的形式暴露出简单的使用名称,以达到public/private的效果。

上述使用原型的时候,都是直接赋值原型对象,这样会覆盖之前已定义好的原型,导致之前原型上的方法或属性丢失,所以通常分开设置/覆盖 一个已知函数的 prototype

User.prototype.update = function() {} 

_proto_


所有 JavaScript 对象(null除外)都有的一个 __proto__ 属性,这个属性指向该对象的原型

function User() {
}
var user1 = new User();
var user2 = new User();
console.log(user1.__proto__ === User.prototype); // true
console.log(user1.__proto__ === user2.__proto__); // true

不管你创建多少个 User 对象实例,他们的原型指向的都是同一个 User.prototype

注: _proto_ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,不建议在生产中使用该属性,我们可以使用ES5的方法 Object.getPrototypeOf 方法来获取实例对象的原型。

Object.getPrototypeOf(user) === User.prototype

当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。

到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined。

function User() {
    this.name = '张三'
}

User.prototype.name = '李四';

Object.prototype.age = 20

var user = new User();
console.log(user.name);  // 张三
console.log(user.age);  // 20

delete user.name
console.log(user.name);  // 李四

如果一个属性在原型链的上端,则对于查找时间将带来不利影响。特别的,试图获取一个不存在的属性将会遍历整个原型链。

并且,当使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问。

所以在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法, 这将会避免原型对象扩展带来的干扰。

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i);
    }
}

构造函数


通过 new 关键字方式调用的函数都被认为是构造函数

function User() { }
var user = new User();
// user.constructor === user.__proto__.constructor 
console.log(user.constructor === User ); // true
console.log(User.prototype.constructor === User); // true

原型 constructor 属性指向构造函数,在构造函数内部,this 指向新创建的对象 Object

如果被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象 - 也就是新创建的对象。

显式的 return 表达式将会影响返回结果,但总是会返回的是一个对象。

function Bar() {
    return 2;
}
var bar = new Bar();  // 返回新创建的对象,而不是数字的字面量 2
console.log(bar.constructor === Bar);  // true
function Foo() {
    this.a = 1;
    
    return {
        b: 2
    };
}

Foo.prototype.c = 3;

var foo = new Foo(); // 返回的对象 {b: 2}
console.log(foo.constructor === Foo);  // false
console.log(foo.a);  // undefined
console.log(foo.b);  // 2
console.log(foo.c);  // undefined

这里得到的 foo 是函数返回的对象,而不是通过new关键字新创建的对象。new Foo() 并不会改变返回的对象 foo 的原型, 也就是返回的对象 foo 的原型不会指向 Foo.prototype 。 因为构造函数的原型会被指向到新创建的对象,而这里的 Foo 没有把这个新创建的对象返回,而是返回了一个包含 b 属性的自定义对象。

如果 new 被遗漏了,则函数不会返回新创建的对象。

function Foo() {
    this.abc = 1; // 获取设置全局参数 this===window
}
Foo(); // undefined

为了不使用 new 关键字,经常会使用工厂模式创建一个对象。

function Foo() {
    var obj = {};
    obj.value = 'blub';

    var private = 2;
    obj.setValue = function(value) {
        this.value = value;
    }

    obj.getPrivate = function() {
        return private;
    }
    return obj;
}

上面的方式看起来出错,并且可以使用闭包来达到封装私有变量, 但是随之而来的是一些不好的地方。

  • 为了实现继承,工厂方法需要从另外一个对象拷贝所有属性
  • 新创建的实例不能共享原型对象上的方法/属性,会占用更多的内存

继承


下面通过 call 实现继承,并将父级 prototype 给子 prototype

function Parent() {
	this.value = 1
}
Parent.prototype.method = function() {
	console.log('value: ' + this.value)
}

function Child() {
	// this -> new Child()
	Parent.call(this);  // 调用Parent构造函数
}

var c1 = new Child();
console.log(c1.value);  // 1

c1.method(); // 报错:Uncaught TypeError: c1.method is not a function

Child.prototype = Parent.prototype;  //继承父方法
var c2 = new Child();

c2.method();  // 'value: 1'

但是这样继承存在一个问题,接上面代码继续

Child.prototype.fn = function() {
    console.log('abc')
}

var p = new Parent();
p.fn();  // 'abc'

因为 Parent.prototype === Child.prototype,原型是同一个引用,可以直接将子类prototype 与 父类分离

for(var i in Parent.prototype) {
	Child.prototype[i] = Parent.prototype[i]
}
@yijinc yijinc changed the title 深入理解 JavaScript 原型 深入理解 JavaScript prototype May 27, 2019
@yijinc yijinc added the blog blogs and articles label Aug 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blog blogs and articles
Projects
None yet
Development

No branches or pull requests

1 participant