Выразительный JavaScript - Марейн Хавербеке 24 стр.


Это даёт нам достаточно элементарных инструментов для написания простых программ. Следующая функция run даёт удобный способ записи и запуска. Она создаёт свежее окружение, парсит и разбирает строчки, которые мы ей передаём, так, как будто они являются одной программой.

function run() {

var env = Object.create(topEnv);

var program = Array.prototype.slice

.call(arguments, 0).join("\n");

return evaluate(parse(program), env);

}

Использование Array.prototype.slice.call – уловка для превращения объекта, похожего на массив, такого как аргументы, в настоящий массив, чтобы мы могли применить к нему join. Она принимает все аргументы, переданные в run, и считает, что все они – строчки программы.

run("do(define(total, 0),",

" define(count, 1),",

" while(<(count, 11),",

" do(define(total, +(total, count)),",

" define(count, +(count, 1)))),",

" print(total))");

// → 55

Эту программу мы видели уже несколько раз – она подсчитывает сумму чисел от 1 до 10 на языке Egg. Она уродливее эквивалентной программы на JavaScript, но не так уж и плоха для языка, заданного менее чем 150 строчками кода.

Функции

Язык программирования без функций – плохой язык.

К счастью, несложно добавить конструкцию fun, которая расценивает последний аргумент как тело функции, а все предыдущие – имена аргументов функции.

specialForms["fun"] = function(args, env) {

if (!args.length)

throw new SyntaxError("Функции нужно тело");

function name(expr) {

if (expr.type != "word")

throw new SyntaxError("Имена аргументов должны быть типа word");

return expr.name;

}

var argNames = args.slice(0, args.length - 1).map(name);

var body = args[args.length - 1];

return function() {

if (arguments.length != argNames.length)

throw new TypeError("Неверное количество аргументов");

var localEnv = Object.create(env);

for (var i = 0; i < arguments.length; i++)

localEnv[argNames[i]] = arguments[i];

return evaluate(body, localEnv);

};

};

У функций в Egg своё локальное окружение, как и в JavaScript. Мы используем Object.create для создания нового объекта, имеющего доступ к переменным во внешнем окружении (своего прототипа), но он также может содержать новые переменные, не меняя внешней области видимости.

Функция, созданная формой fun, создаёт своё локальное окружение и добавляет к нему переменные-аргументы. Затем она интерпретирует тело в этом окружении и возвращает результат.

run("do(define(plusOne, fun(a, +(a, 1))),",

" print(plusOne(10)))");

// → 11

run("do(define(pow, fun(base, exp,",

" if(==(exp, 0),",

" 1,",

" *(base, pow(base, -(exp, 1)))))),",

" print(pow(2, 10)))");

// → 1024

Компиляция

Мы с вами построили интерпретатор. Во время интерпретации он работает с представлением программы, созданным парсером.

Компиляция – добавление ещё одного шага между парсером и запуском программы, которая превращает в программу в нечто, что можно выполнять более эффективно, путём проделывания большинства работы заранее. К примеру, в хорошо организованных языках при каждом использовании переменной очевидно, к какой переменной обращаются, даже без запуска программы. Это можно использовать, чтобы не искать переменную по имени каждый раз, когда к ней обращаются, а напрямую вызывать её из какой-то заранее определённой области памяти.

По традиции компиляция также превращает программу в машинный код – сырой формат, пригодный для исполнения процессором. Но каждый процесс превращения программы в другой вид, по сути, является компиляцией.

Можно было бы создать другой интерпретатор Egg, который сначала превращает программу в программу на языке JavaScript, использует new Function для вызова компилятора JavaScript и возвращает результат. При правильной реализации Egg выполнялся бы очень быстро при относительно простой реализации.

Если вам это интересно, и вы хотите потратить на это время, я поощряю вас попробовать сделать такой компилятор в качестве упражнения.

Мошенничество

Когда мы определяли if и while, вы могли заметить, что они представляли собой простые обёртки вокруг if и while в JavaScript. Значения в Egg – также обычные значения JavaScript.

Сравнивая реализацию Egg, построенную на JavaScript, с объёмом работы, необходимой для создания языка программирования непосредственно на машинном языке, то разница становится огромной. Тем не менее, этот пример, надеюсь, даёт вам представление о работе языков программирования.

И когда вам надо что-то сделать, смошенничать будет более эффективно, нежели делать всё с нуля самому. И хотя игрушечный язык ничем не лучше JavaScript, в некоторых ситуациях написание своего языка помогает быстрее сделать работу.

Такой язык не обязан напоминать обычный ЯП. Если бы JavaScript не содержал регулярных выражений, вы могли бы написать свои парсер и интерпретатор для такого суб-языка.

Или представьте, что вы строите гигантского робота-динозавра и вам нужно запрограммировать его поведение. JavaScript – не самый эффективный способ сделать это. Можно вместо этого выбрать язык примерно такого свойства:

behavior walk

perform when

destination ahead

actions

move left-foot

move right-foot

behavior attack

perform when

Godzilla in-view

actions

fire laser-eyes

launch arm-rockets

Обычно это называют языком для выбранной области (domain-specific language) – язык, специально предназначенный для работы в узком направлении. Такой язык может быть более выразительным, чем язык общего назначения, потому что он разработан для выражения именно тех вещей, которые надо выразить в этой области – и больше ничего.

Упражнения

Массивы

Добавьте поддержку массивов в Egg. Для этого добавьте три функции в основную область видимости: array(...) для создания массива, содержащего значения аргументов, length(array) для возврата длины массива и element(array, n) для возврата n-ного элемента.

// Добавьте кода

topEnv["array"] = "...";

topEnv["length"] = "...";

topEnv["element"] = "...";

run("do(define(sum, fun(array,",

" do(define(i, 0),",

" define(sum, 0),",

" while(<(i, length(array)),",

" do(define(sum, +(sum, element(array, i))),",

" define(i, +(i, 1)))),",

" sum))),",

" print(sum(array(1, 2, 3))))");

// → 6

Замыкания

Способ определения fun позволяет функциям в Egg замыкаться вокруг окружения, и использовать локальные переменные в теле функции, которые видны во время определения, точно как в функциях JavaScript.

Следующая программа иллюстрирует это: функция f возвращает функцию, добавляющую её аргумент к аргументу f, то есть, ей нужен доступ к локальной области видимости внутри f для использования переменной a.

run("do(define(f, fun(a, fun(b, +(a, b)))),",

" print(f(4)(5)))");

// → 9

Объясните, используя определение формы fun, какой механизм позволяет этой конструкции работать.

Комментарии

Хорошо было бы иметь комментарии в Egg. К примеру, мы могли бы игнорировать оставшуюся часть строки, встречая символ \# – так, как это происходит с // в JavaScript.

Большие изменения в парсере делать не придётся. Мы просто поменяем skipSpace, чтобы она пропускала комментарии, будто они являются пробелами – и во всех местах, где вызывается skipSpace, комментарии тоже будут пропущены. Внесите это изменение.

// Поменяйте старую функцию

function skipSpace(string) {

var first = string.search(/\S/);

if (first == -1) return "";

return string.slice(first);

}

console.log(parse("# hello\nx"));

// → {type: "word", name: "x"}

console.log(parse("a # one\n # two\n()"));

// → {type: "apply",

// operator: {type: "word", name: "a"},

// args: []}

Чиним область видимости

Сейчас мы можем присвоить переменной значение только через define. Эта конструкция работает как при присвоении старым переменным, так и при создании новых.

Эта неоднозначность приводит к проблемам. Если вы пытаетесь присвоить новое значение нелокальной переменной, вместо этого вы определяете локальную с таким же именем. (Некоторые языки так и делают, но мне это всегда казалось дурацким способом работы с областью видимости).

Добавьте форму set, схожую с define, которая присваивает переменной новое значение, обновляя переменную во внешней области видимости, если она не задана в локальной. Если переменная вообще не задана, швыряйте ReferenceError (ещё один стандартный тип ошибки).

Техника представления областей видимости в виде простых объектов, до сего момента бывшая удобной, теперь будет вам мешать. Вам может понадобиться функция Object.getPrototypeOf, возвращающая прототип объекта. Также помните, что область видимости не наследуется от Object.prototype, поэтому если вам надо вызвать на них hasOwnProperty, придётся использовать такую неуклюжую конструкцию:

Object.prototype.hasOwnProperty.call(scope, name);

Это вызывает метод hasOwnProperty прототипа Object и затем вызывает его на объекте scope.

specialForms["set"] = function(args, env) {

// Ваш код

};

run("do(define(x, 4),",

" define(setx, fun(val, set(x, val))),",

" setx(50),",

" print(x))");

// → 50

run("set(quux, true)");

// → Ошибка вида ReferenceError

12. JavaScript и браузер

Браузер – крайне враждебная программная среда

Дуглас Крокфорд, "Язык программирования JavaScript" (видеолекция)

Следующая часть книги расскажет о веб-браузерах. Без них не было бы JavaScript. А если бы и был, никто бы не обратил на него внимания.

Технологии веба с самого начала были децентрализованными – не только технически, но и с точки зрения их эволюции. Различные разработчики браузеров добавляли новую функциональность "по случаю", непродуманно, и часто эта функциональность обретала поддержку в других браузерах и становилась стандартом.

Это и благословение и проклятие. С одной стороны, здорово не иметь контролирующего центра, чтобы технология развивалась различными сторонами, иногда сотрудничающими, иногда конкурирующими. С другой – бессистемное развитие языка привело к тому, что результат не является ярким примером внутренней согласованности. Некоторые части привносят путаницу и беспорядок.

Сети и интернет

Компьютерные сети появились в 1950-х. Если вы проложите кабель между двумя или несколькими компьютерами и разрешите им передавать данные, вы может делать много удивительных вещей. А если связь двух машин в одном здании позволяет делать много разного, то связь компьютеров по всей планете должна позволять ещё больше. Технология, позволяющая это сделать, была создана в 1980-х, и получившаяся сеть зовётся интернетом. И она оправдала ожидания.

Компьютер может использовать эту сеть, чтобы кидаться битами в другой компьютер. Чтобы общение вышло эффективным, оба компьютера должны знать, что эти биты означают. Значение любой заданной последовательности битов зависит от того, что пытаются ими выразить, и какой механизм кодирования используется.

Стиль общения по сети описывает сетевой протокол. Есть протоколы для отправки e-мейлов, для получения e-мейлов, для распространения файлов и даже для контроля над компьютерами, заражёнными вредоносным софтом.

К примеру, простой протокол чата может состоять из одного компьютера, отправляющего биты, представляющие текст "ЧАТ?" на другой, а второго отвечающего текстом "ОК!", для подтверждения того, что он понял протокол. Дальше они могут перейти к отправке друг другу текстов, чтения полученных текстов и вывода их на экран.

Большинство протоколов построено на основе других протоколов. Наш протокол чата из примера рассматривает сеть как потоковое устройство, в которое можно вводить биты и заказывать их приход на конкретный адрес в правильном порядке. А обеспечение этого процесса – само по себе является сложной задачей. Transmission Control Protocol (TCP) – протокол, решающий эту задачу. Все устройства, подключённые к интернету, говорят на нём, и большинство общения в интернете построено на его основе.

Соединение по TCP работает так: один компьютер ждёт, или "слушает", пока другие не начнут с ним говорить. Чтобы можно было слушать разные виды общения в одно и то же время, для каждого из них назначается номер (называемый портом). Большинство протоколов устанавливают порт, используемый по умолчанию. К примеру, если мы отправляем е-мейл по протоколу SMTP, компьютер, через который мы его шлём, должен слушать порт 25.

Тогда другой компьютер может установить соединение, связавшись с компьютером назначения по правильному порту. Если машина назначения доступна, и она слушает этот порт, соединение устанавливается. Слушающий компьютер зовётся сервером, а соединяющийся – клиентом.

Такое соединение работает как двусторонняя труба, по которой текут биты – обе машины могут помещать в неё данные. Когда биты переданы, другая машина может их прочесть. Это удобная модель. Можно сказать, что TCP обеспечивает абстракцию сети.

Веб

World Wide Web, всемирная паутина (это не то же самое, что весь интернет в целом) – набор протоколов и форматов, позволяющий нам посещать странички через браузер. Web (рус. "паутина") в названии обозначает, что страницы можно легко связать друг с другом, в результате чего образуется гигантская сеть-паутина, по которой движутся пользователи.

Чтобы добавить в Веб содержимое, вам нужно соединить машину с интернетом и заставить её слушать 80 порт, используя протокол передачи гипертекста, Hypertext Transfer Protocol (HTTP). Он позволяет другим компьютерам запрашивать документы по сети.

Каждый документ имеет имя в виде универсального локатора ресурсов, Universal Resource Locator (URL), который выглядит примерно так:

http://eloquentjavascript.net/12_browser.html

| | | |

протокол сервер путь

Первая часть говорит нам, что URL использует протокол HTTP (в отличие от, скажем, зашифрованного HTTP, который записывается как https://). Затем идёт часть, определяющая, с какого сервера мы запрашиваем документ. Последняя – строка пути, определяющая конкретный документ или ресурс.

У каждой машины, присоединённой к интернету, есть свой адрес IP, который выглядит как 37.187.37.82. Его иногда можно использовать вместо имени сервера в URL. Но цифры сложнее запоминать и печатать, чем имена – поэтому обычно вы регистрируете доменное имя, которое указывает на конкретную машину (или набор машин). Я зарегистрировал eloquentjavascript.net, указывающий на IP-адрес машины, которую я контролирую, поэтому можно использовать этот адрес для предоставления веб-страниц.

Если вы введёте указанный URL в адресную строку браузера, он попробует запросить и показать документ, находящийся по этому URL. Во-первых, браузеру надо выяснить, куда ссылается домен eloquentjavascript.net. Затем, используя протокол HTTP, он соединяется с сервером по этому адресу, и спрашивает его ресурс по имени /12_browser.html

В главе 17 мы подробнее рассмотрим протокол HTTP.

Назад Дальше