重读《JavaScript高级程序设计》(2)

本文同步本人掘金平台的文章:https://juejin.cn/post/6844903569317953550

继承

许多的OO语言都支持两种继承方法:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且实现主要是依靠原型链来实现的。[p162]

原型链

原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。回顾下构造函数、原型和实例的关系: 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

function SuperType(){
	this.property = true;
}
SuperType.prototype.getSuperValue = function(){
	return this.property;
}
function SubType(){
	this.subProperty = false;
}

// 继承了SuperType,重点哦
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
	return this.subProperty;
}

var instance = new SubType();
console.log(instance.getSuperValue()); // true
复制代码

原型链继承带来两个问题:一是原型实际上变成了另一个类型的实例,于是,原先的实例属性也就变成了现在原型的属性,共享了属性。二是在创建子类型的实例时,不能在没有影响所有对象实例的情况下向超类型的构造函数传递参数。

借用构造函数

借用构造函数解决原型链继承带来的不能向构造函数传递仓鼠的问题。这里使用到了apply()或者call()方法在新创建的对象上执行构造函数。

function SuperType(){
	this.colors = ['red','blue','green'];
}
function SubType(){
	// 继承SuperType
	SuperType.call(this); // SuperType.apply(this)同效
}

var instance1 = new SubType();
instance1.color.push('black');
console.log(instance1.colors); // 'red,blue,green,black'

var instance2 = new SubType();
console.log(instance2.colors); // 'red,blue,green'
复制代码

上面的例子中,我在父类型构造函数中没有传参数,看者感兴趣的话可以自己加下参数来实验一番咯。

借用构造函数解决了原型链继承的确定,但是又没有接纳原型链的优点:共享。下面的组合继承结合了原型链和借用构造函数,容纳了两者的优点。

组合继承

组合继承的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承

function SuperType(name){
	this.name = name;
	this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
	console.log(this.name);
}
function SubType(name,age){
	// 继承属性
	SuperType.call(this,name);
	this.age = age;
}

// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor =SubType; // 避免重写构造函数指向错误
SubType.prototype.sayAge = function(){
	console.log(this.age);
}

var instance1 = new SubType('nicholas' , 29);
instance1.colors.push('black');
console.log(instance1.colors); // 'red,blue,green,black'
instance1.sayName(); // 'nicholas'
instance1.sayAge(); // 29

var instance2 = new SubType('greg' , 27);
console.log(instance2.colors); // 'red,blue,green'
instance2.sayName(); // 'greg'
instance2.sayAge(); // 27
复制代码

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为了JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。

原型式继承

原型式继承是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义的类型。

function object(o){ // 传入一个对象
	function F(){};
	F.prototype = o;
	return new F();
}

var person = {
	name : 'nicholas',
	friends: ['shelby','court','van']
};

var anotherPerson = object(person);
anotherPerson.name = 'greg';
anotherPerson.friends.push('rob');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'linda';
yetAnotherPerson.friends.push('barbie');

console.log(person.friends); // 'shelby,court,van,rob,barbie'
复制代码

寄生式继承

寄生式继承是与原型继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即是创建了一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的做了所有工作一样返回对象。

function object(o){ // 传入一个对象
	function F(){};
	F.prototype = o;
	return new F();
}
function createAnother(original){
	var clone = object(original);
	clone.sayHi = function(){
		console.log('hi');
	};
	return clone;
}
var person = {
	name : 'nicholas',
	friends : ['shelby','court','van']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'hi'
复制代码

上面的例子中,新对象anotherPerson不仅具有person的所有属性和方法,而且还有了自己的sayHi()方法。

寄生组合式继承

组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。寄生组合式继承能够解决这个问题。

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型的原型的一个副本而已。寄生组合式继承的基本模式如下:

function inheritPrototype(subType,superType){
	var prototype = Object(superType.prototype); // 创建对象
	prototype.constructor = subType; // 增强对象,防止下面重写constructor属性
	subType.prototype = prototype; // 指定对象
	
}
复制代码

一个完整的例子如下,相关插图见书[p173]:

function inheritPrototype(subType,superType){
	var prototype = Object(superType.prototype);
	prototype.constructor = subType;
	subType.prototype = prototype;
	
}
function SuperType(name){
	this.name = name;
	this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
	alert(this.name);
}
function SubType(name, age){
	SuperType.call(this, name); // 只在这调用了一次超类型的构造函数
        this.age = age;
}

inheritPrototype(SubType , SuperType);

SubType.prototype.sayAge = function(){
	console.log(this.age);
}

var instance = new SubType('nicholas' , 29);
复制代码

上面的例子的高效处体现在它只调用了一次SuperType构造函数,并且避免了在SubType.prototype上创建不必要的,多余的属性。与此同时,原型链还能保持不变;因此还能正常使用instanceof和inPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。我的理解是,函数内的函数使用到外层函数的变量延长变量的生存时间,造成常驻内存。例子见下:

function foo(){
    var a = 2;
    return function(){
		a += 1;
		console.log(a);
	}
}

var baz = foo();

baz(); // 3
baz(); // 4
baz(); // 5
baz(); // 6
复制代码

上面的例子中,外部的函数foo()执行完成之后,正常的情况下应该销毁a变量的,但是内部的返回的匿名函数使用到该变量,不能销毁。如果需要销毁的话,可以改写成下面:

function foo(){
	var a = 2;
	return function(){
		a += 1;
		console.log(a);
	}
}
var baz = foo();
baz(); // 3

baz = null; // 将内部的匿名函数赋值为空
复制代码

从闭包说起

谈到了闭包,这让我想起了不久前刷知乎看到一篇文章。自己整理如下:

for(var i = 0 ; i < 5; i++){
	setTimeout(function(){
		console.log(i);
	},1000)
}
console.log(i);

// 5,5,5,5,5,5
复制代码

上面的代码是输出了6个5,而这6个5是这样执行的,先输出全局中的console.log(i),然后是过了1秒种后,瞬间输出了5个5(为什么用瞬间这个词呢,怕看者理解为每过一秒输出一个5)。解读上面的代码的话,可以通过狭义范围(es5)的理解:同步 => 异步 => 回调 (回调也是属于异步的范畴,所以我这里指明了狭义啦)。先是执行同步的for,遇到异步的setTimeout(setTimeout和setInterval属于异步哈)后将其放入队列中等待,接着往下执行全局的console.log(i),将其执行完成后执行异步的队列。

追问1:闭包

改写上面的代码,期望输出的结果为:5 => 0,1,2,3,4。改造的方式一:

for(var i = 0; i < 5; i++){
	(function(j){
		setTimeout(function(){
			console.log(j);
		},1000);
	})(i);
}
console.log(i);

// 5,0,1,2,3,4
复制代码

上面的代码巧妙的利用IIFE(Immediately Invoked Function Expression:声明即执行的函数表达式)来解决闭包造成的问题,闭包的解析看上面。

方法二:利用js中基本类型的参数传递是按值传递的特征,改造代码如下

var output = function(i){
	setTimeout(function(){
		console.log(i);
	},1000);
};
for(var i = 0; i < 5; i++){
	output(i); // 这里传过去的i值被复制了
}
console.log(i);

// 5,0,1,2,3,4
复制代码

上面改造的两个方法都是执行代码后先输出5,然后过了一秒种依次输出0,1,2,3,4。

如果不要考虑全局中的console.log(i)输出的5,而是循环中输出的0,1,2,3,4。你还可以使用ES6的let块级作用域语法,实现超级简单:

for(let i = 0; i < 5; i++){
	setTimeout(function(){
		console.log(i);
	},1000);
}

// 0,1,2,3,4
复制代码

上面是过了一秒钟后,依次输出0,1,2,3,4。这种做法类似于无形中添加了闭包。那么,如果使用ES6语法的话,会怎样实现5,0,1,2,3,4呢?

追问2:ES6

改造刚开始的代码使得输出的结果是每隔一秒输出0,1,2,3,4,大概第五秒输出5。

在不使用ES6的情况下:

for(var i = 0; i < 5; i++){
	(function(j){
		setTimeout(function(){
			console.log(j);
		},1000*j);
	})(i);
}
setTimeout(function(){
	console.log(i);
},1000*i);

// 0,1,2,3,4,5
复制代码

上面的代码简单粗暴,但是不推荐。看题目是每隔一秒输出一个值,再回调实现最后的5输出,这个时候应该使用ES6语法来考虑,应该使用Promise方案:

const tasks = [];
for(var i = 0; i < 5; i++){// 这里的i声明不能改成let,改成let的话请看下一段代码
	((j)=>{
		tasks.push(new Promise((resolve)=>{ // 执行tasks
			setTimeout(()=>{
				console.log(j);
				resolve(); // 这里一定要resolve,否则代码不会按照预期执行
			},1000*j);
		}))
	})(i);
}

Promise.all(tasks).then(()=>{ // 执行完tasks,回调
	setTimeout(()=>{
		console.log(i);
	},1000);
});

// 符合要求的每隔一秒输出
// 0,1,2,3,4,5
复制代码

如果是使用let,我的改造如下:

const tasks = [];
for (let i = 0; i < 5; i++) {
		tasks.push(new Promise((resolve) => {
			setTimeout(() => {
				console.log(i);
				resolve();
			}, 1000 * i);
		}));
}

Promise.all(tasks).then(() => {
	setTimeout(() => {
		console.log(tasks.length);
	}, 1000);
});

// 0,1,2,3,4,5
复制代码

上面的代码比较庞杂,可以将其颗粒话,模块化。对上面两段代码的带var那段进行改造后如下:

const tasks = []; // 这里存放异步操作的Promise
const output = (i) => new Promise((resolve) => {
	setTimeout(()=>{
		console.log(i);
	},1000*i);
});

// 生成全部的异步操作
for(var i = 0; i < 5; i++){
	tasks.push(output(i));
}
// 异步操作完成之后,输出最后的i
Promise.all(tasks).then(() => {
	setTimeout(() => {
		console.log(i);
	},1000);
});

// 符合要求的每隔一秒输出
// 0,1,2,3,4,5
复制代码

追问3:ES7

既然ES6的Promise可以写,那么ES7是否可以写呢,从而让代码更加简洁易读?那就使用到到了异步操作的async await特性啦。

// 模拟其他语言中的sleep,实际上可以是任何异步操作
const sleep = (time) => new Promise((resolve) => {
	setTimeout(resolve , time);
});

(async () => {
	for(var i = 0; i < 5; i++){
		await sleep(1000);
		console.log(i);
	}
	
	await sleep(1000);
	console.log(i);
})();

// 符合要求的每隔一秒输出
// 0,1,2,3,4,5
复制代码

浏览器窗口位置

IE、Safari、Opera和Chrome都提供了screenLeft和screenTop属性,分别表示浏览器窗口相对于屏幕左上角和上边的位置[p197]。Firefox则以screenX和screenY属性来表示。为了兼容各个浏览器,可以入下面这样写:

var leftPos = (typeof window.screenLeft == "number")?window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number")? window.screenTop : window.screenY;
复制代码

浏览器窗口大小

由于浏览器厂商以及历史的问题,无法确认浏览器本身的大小,但是可以取得视口的大小[p198]。如下:

var pageWidth = window.innerWidth,
    pageHeight = window.innerHeight;
    
if(typeof pageWidth != "number"){
	if(document.compatMode == 'CSS1Compat'){ // 标准模式下的低版本ie
		pageWidth = document.documentElement.clientWidth;
		pageHeight = document.documentElement.clientHeight;
	}else{ // 混杂模式下的chrome
		pageWidth = document.body.clientWidth;
		pageHeight = document.body.clientHeight;
	}
}
复制代码

上面的示例可以简写成下面这样:

var pageWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientHeight;
var pageHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
复制代码

canvas中的变换

为绘制上下文应用变换,会导致使用不同的变换矩阵应用处理,从而产生不同的结果。[p453]

可通过下面的方法来修改变换矩阵:

  • rotation(angle):围绕原点旋转图像angle弧度
  • scale(scaleX,scaleY)
  • translate(x,y): 将坐标原点移动到(x,y)。执行这个变换后,坐标(0,0)会变成之前由(x,y)表示的点。

JSON

关于JSON,最重要的是要理解它是一种数据格式,不是一种编程语言。

对象字面量和JSON格式比较

先来看下对象字面量demo写法:

var person = {
	name : "nicholas",
	age : 29
};

# 上面的代码也可以写成下面的
var person = {
	"name" : "nicholas",
	"age" : 29
};
复制代码

而上面的对象写成数据的话,就是下面这样了:

{
	"name": "nicholas ",
	"age": 29
}

# 可到网站 https://www.bejson.com/ 验证
复制代码

⚠️ 与JavaScript对象字面量相比,JSON对象又两个地方不一样。首先,没有声明变量(JSON中没有变量的概念)。其次,没有分号(因为这不是JavaScript语句,所以不需要分号)。留意的是,对象的属性必须加双引号(不是单引号哦),这在JSON中是必须的。

stringify()和parse()

可以这么理解:JSON.stringify()是从一个object中解析成JSON数据格式,而JSON.parse()是从一个字符串中解析成JSON数据格式。

var person = {
	name: 'nicholas',
	age: 29
};

var jsonText = JSON.stringify(person);

console.log(jsonText);

// {"name":"nicholas","age":29}
复制代码
var strPerson = '{"name":"nicholas","age":29}';
var jsonText = JSON.parse(strPerson);

console.log(jsonText); // { name: 'nicholas', age: 29 }
复制代码

XMLHttpRequest对象

XMLHttpRequest对象用于在后台与服务器交换数据。它是Ajax技术的核心[p571]。

XMLHttpRequest对象能够使你:

  • 在不重新加载页面的情况下更新网页
  • 在页面已加载后从服务器请求数据
  • 在页面已加载后从服务器接收数据
  • 在后台向服务器发送数据

XMLHttpRequest的使用:

# 创建XHR对象 => open()准备发送 => send()传送数据

// 创建对象,对浏览器做兼容
function createXHR(){
	if(typeof XMLHttpRequest != 'undefined'){ // IE7+和其他浏览器支持
		return new XMLHttpRequest();
	}else if(typeof ActiveXObject != 'undefined'){
		if(typeof arguments.callee.activeXString != 'string'){
			var versions = ['MSXML2.XMLHttp.6.0','MSXML2.XMLHttp.3.0','MSXML2.XMLHttp']; // 低版的ie可能遇到三种不同版本的XMR对象
			var i , len;
			for(i = 0,len = versions.length; i < len ; i++){
				try{
					new ActiveXObject(version[i]);
					arguments.callee.activeXString = versions[i];
					break;
				}catch (ex){
					// 跳过
				}
			}
		}
		return new ActiveXObject(arguments.callee.activeXString);
	}else{
		throw new Error("No XHR object available.");
	}
}
var xhr = createXHR();

// 准备发送数据
xhr.open("get","path/to/example.txt",false);// 非异步,异步的话第三个参数改为true

// 传送数据
xhr.send(null); // get方法不需要传数据

// 判断状态嘛,获取服务器返回的数据
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
	console.log(xhr.responseText);
}else{
	console.log("Request was nsuccessful : " + xhr.status);
}
复制代码

跨域解决方案

何为跨域呢?只要访问的资源的协议、域名、端口三个不全相同,就可以说是非同源策略而产生了跨域了,这是狭义的说法。广义的说法:通过XHR实现Ajax通信的一个主要限制,来源于跨域的安全策略;默认情况下,XHR对象只能访问包含它的页面位于同一个域中的资源[p582]。注:部分文字和代码引用自前端常见跨域解决方案(全)

CORS

CORS(Cross-Origin Resource Sharing,跨资源共享)定义了在必须访问跨资源时,浏览器与服务器应该如何沟通。其背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。 复杂的跨域请求应当考虑使用它。

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无需设置,如果要带cookie请求:前后端都要设置。

1.前端设置

1.) 原生ajax

function createCORSRequest(method,url){ // 兼容处理,ie8/9需要用到window.XDomainRequest
	var xhr = new XMLHttpRequest();
	// 前端设置是否带cookie
	xhr.withCredentials = true;
	
	if("withCredentials" in xhr){ // 其他的用到withCredentials
		xhr.open(method,url,true);
	}else if(typeof XDomainRequest != 'undefined'){
		xhr = new XDomainRequest();
		xhr.open(method , url);
	}else{
		xhr = null;
	}
	
	return xhr;
}

// get请求
var request = createCORSRequest("get","http://www.somewhere-else.com/page/");
if(request){
	request.onload = function(){
		//  对request.responseText 进行处理 
	};
	request.send();
}

// post请求,带cookie
var requestXhr = createCORSRequest("post","http://www.somewhere-else.com/page/");
requestXhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
requestXhr.send("user=admin");
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};
复制代码

2.)jquery ajax

上面写了一大堆原生的,看得头都有点大了,还是使用jquery ajax 比较舒服:

$.ajax({
	...
	xhrFields: {
		withCredentials: true // 前端设置是否带cookie
	},
	crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
	...
});
复制代码

3.) vue框架

在vue-resource封装的ajax组建中加入以下代码:

Vue.http.options.credentials = true;
复制代码

2.服务器设置

若后端设置成功,前端浏览器控制台上就不会出现跨域报错的信息,反之,说明没有成功。

1.) java后台

/*
 * 导入包:import javax.servlet.http.HttpServletResponse;
 * 接口参数中定义:HttpServletResponse response
 */
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");  // 若有端口需写全(协议+域名+端口)
response.setHeader("Access-Control-Allow-Credentials", "true");
复制代码

2.) node后台

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');
复制代码

JSONP

JSONP是JSON with padding(填充式JSON或参数式JSON)的简写,是应用JSON的一种新方法,在后来的web服务中非常流行。简单的跨域请求用JSONP即可。

通常为了减轻web服务器的负载,我们把js,css,img等静态资源分离到另一台独立域名的服务器,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。

1.前端实现

1.)原生实现


复制代码

服务器返回如下(返回时即执行全局函数):

onBack({"status": true,"user":"admin"})
复制代码

2.)jquery ajax

$.ajax({
	url: 'http://www.domain2.com:8080/login',
	type: 'get',
	dataType: 'jsonp', // 请求方式为jsonp 
	jsonpCallback: 'onBack', // 自定义回调函数名
	data: {}
});
复制代码

3.)vue.js

this.$http.jsonp('http://www.domain2.com:8080/login',{
	params: {},
	jsonp: 'onBack '
}).then((res)=>{
	console.log(res);
});
复制代码

2.后端nodejs代码的示范:

var qs = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request',function(req,res){
	var params = qs.parse(req.url.split('?')[1]);
	var fn = params.callback;
	
	// jsonp返回设置
	res.writeHead(200,{"Content-Type":"text/javascript"});
	res.write(fn + '('+JSON.stringify(params)+')');
	
	res.end();
});

server.listen('8080');
console.log('Server is running at port 8080 ...');
复制代码

⚠️ jsonp缺点:只能实现get一种请求。

WebSocket协议跨域

WebSocket protocol 是 HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯。

原生的WebSocket API使用起来不太方便,示例中使用了socket.io,它很好的封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

1.前端代码

user input:


复制代码

2.node socket后台

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});
复制代码

requestAnimationFrame()帧动画

requestAnimationFrame 创建平滑的动画[p682]。在此之前都是使用setTimeout或者setInterval实现,requestAnimationFrame与它们相比:

  • 不需要时间间隔,会贴切浏览器的刷新频率
  • 在切换到另外的页面时,会停止运行

使用的示范如下:

1
复制代码
//  兼容浏览器
(function(){
    var lastTime = 0;
    var vendors = ['webkit','moz','ms','-o'];
    for(var x = 0;x 
发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章